Creating Views in the Thinkwise Software Factory
Reference for the view lifecycle covered by this skill: tab (type view) → columns → references
→ control procedure → template → template_prog_object_item → generated CREATE VIEW program
object, generated and verified. Deployment (pushing the generated code to an actual database) is a
separate concern this skill does not cover.
Apply this whenever an MCP connector with Software Factory access (sf_mcp, indicium) is used to
create, inspect, or convert a query into a view — follow the connector's standard discovery→act flow;
never guess entity/task/property names. tab/col typically live in a data_modeling-style domain;
control_proc/control_proc_template/template_prog_object_item typically live in a
manage_model-style domain — try these directly first, and only escalate to
search_capabilities/get_available_domains on an entity_set_not_found/domain_not_found-style
rejection rather than re-discovering a domain that already resolved earlier this session. For
the general control-procedure/template/assignment mechanics referenced throughout (code groups,
[PARMTR] substitution, static vs. SQL assignment, dialect differences), see the
thinkwise_software_factory_create_control_procedures skill — this skill only covers what's specific
to views.
What a view is
A view is a tab entity with type_of_table = view (enum: table 0, view 1, function 2,
mqt 4). Like a table it has columns, a primary key, references, screens, tasks, security —
everything a table has — except the data is never stored; it's composed at query time from a
SELECT statement, so it's always current and never needs syncing.
tab.create_view_method picks how that SELECT gets defined (enum: meta_auto 0, meta_custom 1,
template 2):
| Method |
What you write |
When to use |
Meta Auto (meta_auto) |
Nothing — map each view column to a source column (col.view_tab_id/col.view_col_id), run the task_generate_view_from_clause bound task, the FROM/join logic (tab.view_from_clause) is derived automatically |
Only a straight pull from one or a few tables joined on their modeled references, no filtering, no aggregation |
Meta Custom (meta_custom) |
Hand-edit tab.view_from_clause/view_where_clause/view_grp_by_clause/view_having_clause; the SELECT clause is still derived from the view's columns |
Rare middle ground — custom joins/filters but still want the platform to build the select list |
Template (template) |
The entire SELECT statement, written as a control procedure template |
Anything with real business logic: multi-table joins, aggregation, calculated fields, UNION, conditional logic |
Default to Template. In a large, mature production model inspected directly (210 views), every
single one used create_view_method = template — zero used Meta Auto or Meta Custom. A view earning
its keep almost always needs a join condition, a filter, or a derived column the auto-generated
FROM clause can't express, so teams converge on Template as the standard rather than starting with
Meta Auto and migrating later.
A fourth type_of_table value, mqt (materialized query table / snapshot), persists and
periodically refreshes the result instead of computing it live. Reach for it only once a view's
underlying query becomes a measurable, repeated performance cost — don't start there pre-emptively.
Naming
Base rules match tables generally: singular, self-explanatory, underscore-separated, no
abbreviations, no meta-information baked into the name. View-specific points:
- Name the view for the business question it answers, not its mechanics —
customer_list,
additional_cost, not join_customer_and_customer_detail.
- Reserve a prefix for platform-generated/dynamic-model view categories, used consistently. The
Thinkwise report-label pattern is the canonical example: any view feeding translated report labels
is named with the
rpt_lbl_ prefix so the dynamic-model concept can find it. If a model has its
own generated-view categories (audit/history, staging, …), give each one fixed prefix and document
it — don't let it drift per developer.
- Current official guidance is lowercase
snake_case (customer_list, not Customer_List).
At least one large production model instead uses PascalCase_With_Underscores throughout (a
legacy convention predating current guidelines). Follow whatever the model already uses; for a new
model, follow the lowercase guideline. Consistency within one model matters more than which style
is picked.
- Name the control procedure and template after the view so the code producing a view's data is
trivially discoverable from the view's name. A bare match (
control_proc_id == tab_id) is
simplest and needs no explaining — pick one convention (bare match, or a fixed prefix like Vw_)
and hold to it across the model; don't mix both.
Confirm scope before modeling
Before creating the tab row, state the plan back to the user and get their confirmation — this
applies to every view being designed, not only an ad hoc-query conversion (see below). This is the
thinkwise_software_factory_mcp_base "Confirm-before-mutate" convention applied at this skill's own
grain; it isn't superseded by anything below about the view's own setup mechanics:
- Source tables/columns the view pulls from.
- Intended grain — what one row of the view is meant to represent.
- Primary-key candidate — the column(s) that define that grain.
create_view_method — Template vs. Meta Auto vs. Meta Custom (default to Template — see
above).
Flag, don't silently decide, if the request is a poor fit for a plain view — heavy aggregation
over a large table, queried by dozens of other things, on source data that barely changes. A
snapshot (type_of_table = mqt) or a scheduled denormalized table is often the better trade; raise
it with the user rather than defaulting to a view.
Setting up a view — the basics
- Data model → Tables → New table. Set
type_of_table = view, pick create_view_method
(default template).
- Model the columns before writing any code. For Template views, the control procedure's
SELECT list must match the view's modeled column list, in the same order — model columns first,
then write SELECT to match, not the other way around.
- Column order: same 10-per-step convention as regular tables (
col.order_no 10, 20, 30, …),
leaving gaps for later insertions.
- Primary key: pick the column(s) that define the view's actual grain (the level of
detail one row represents) — usually the query's
GROUP BY/DISTINCT columns, or the source
table's PK for a row-for-row pull. If the grain isn't obvious from the request, ask the user what
one row of the view is meant to represent before picking the columns/primary key. Getting the
grain wrong (a PK that isn't actually unique per row) is the most common view bug — nothing
enforces uniqueness on a view by default, so verify manually against real data
(GROUP BY <candidate PK> HAVING COUNT(*) > 1) before shipping.
- Reuse existing domains for every column representing the same concept as an existing column
elsewhere in the model — see next section.
- References are part of modeling the view, not an optional follow-up. For every column on the
view that is FK-shaped (its value is meant to match another table's primary key — e.g. an
employee_id column, or an absence_id-style pointer), add the ref/ref_col at this point,
before writing the query. They are never implied by the view's FROM clause; Meta Auto/Meta
Custom infer references from mapped source columns, but Template gives none for free — skip this
step on a Template view and the column silently has no look-up, no detail grid, and no
auto-derived OData/Indicium navigation, even though the generated SELECT still returns the right
data.
See the thinkwise_datamodeling_guidelines skill's "References (foreign keys)" section for the
full look-up-vs-detail explanation. The one point specific to views, repeated here because it's
easy to get backwards: the direction is reversed from a normal table-to-table FK. For a view
column pointing at a real table's primary key, source_tab_id = the real table, target_tab_id
= the view — not the other way around. Getting this backwards makes the reference render as a
detail grid on the view instead of the look-up you almost always want there.
Example — view employee_absence_overview, column employee_id pointing at employee:
ref: source_tab_id = employee, target_tab_id = employee_absence_overview,
check_ref = false, show_detail = false
ref_col: source_col_id = employee_id, target_col_id = employee_id
check_ref = false because a view carries no physical FK constraint; show_detail = false
because a detail grid on the real table listing view rows is rarely useful. If two or more view
columns point at the same target table, set ref_add on each (e.g. "most_recent" /
"upcoming") so their auto-derived ref_ids don't collide.
- Write and assign the actual query (below).
- Regenerate and verify the generated code (below).
The new view and its columns need translating, same as any table. tab/col created here get
the platform's usual bracket-placeholder translation until filled in — see
thinkwise_software_factory_translation_objects for the detection query and field reference. Do
this after the column list is finalized (step 2) so it isn't repeated every time a column gets
renamed while the view is still being modeled.
Reusing columns and domains
A view column is not "read-only, so it doesn't matter" — every domain decision made for the source
tables should carry through:
- Match the domain (
col.dom_id) of the source column exactly, not just the data type. If
customer.customer_code uses domain Customer_Code, the corresponding view column surfacing the
customer code should use the same Customer_Code domain — not a fresh domain with the same
NVARCHAR(20) shape. This keeps default UI controls, translations, input constraints, and
domain-level validation consistent everywhere the value appears. Verified in a real production
view: Customer_Code, Customer_Group, Currency, Variety_No and other columns all reuse the
exact same domain IDs as their source tables; generic domains like a date domain or a datetime
domain get reused wherever a date/datetime is surfaced, rather than each view minting its own.
- Only introduce a new domain for a genuinely new business concept — a computed/derived value
with no 1:1 existing column (a calculated total, a concatenated display string, a derived flag).
Don't create a "view-only" duplicate of an existing domain out of convenience.
- Aggregations/calculations should still reuse the base domain where the semantics match (a
summed amount column can use the same currency/amount domain as the column being summed) — only
diverge if the aggregation changes the meaning (a
COUNT result is a plain integer, not whatever
domain the counted column used).
- FK-shaped columns inside a view keep the same column name as the table they reference, exactly
like normal FK columns — this is what lets Indicium/OData auto-derive the relationship and keeps
look-ups working without extra configuration.
Writing and assigning code to a view (Template method)
This is the verified, concrete object chain behind every Template view in a production model, traced
end-to-end via the Software Factory metadata API:
tab (view) → generation produces a structural prog_object named view_<tab_id>, owned by the
framework's VIEWS-code-group meta control procedure → your own control_proc supplies the actual
SELECT as a fragment woven into that prog_object via a template_prog_object_item row.
Query branch_rdbms_type (/branch_rdbms_type?$filter=model_id eq '<model>' and branch_id eq '<branch>' — see the control-procedures skill's "Check branch_rdbms_type first" section) before
writing anything. One row → write the view's SELECT in that platform's dialect. More than one row
→ this view needs one dialect-specific control_proc_template per platform and one
template_prog_object_item per (rdbms_type, prog_object_id) — decide this now, it changes steps 3
and 5 below, not just the SQL text in step 4. Skipping this is exactly how a view ends up with
getdate()/dateadd/select top n generated against a PostgreSQL-only model, or limit/age()
generated against a SQL-Server-only one — every write along the way still reports success either
way, so this has to be checked up front, not diagnosed after the fact.
Model the view's columns first (above) — order, primary key, domains. The SELECT written in
step 5 must produce exactly this column list, in this order, with matching aliases.
Make sure the view's structural program object exists. A brand-new view has no generated
program objects yet. Run Generate code group for the VIEWS code group (task_generate_code_grp,
bound to control_proc, addressable by just (model_id, branch_id, control_proc_id) — use any
control procedure already in VIEWS, e.g. the framework's own pg_views, since your own control
procedure for this view doesn't exist yet at this point) once so the framework's view_<tab_id>
program object shell exists — nothing can be attached to it before that. This only creates the
placeholder row — it does not generate any code yet; see step 6 and the
thinkwise_software_factory_create_control_procedures skill's "Actually generating code" section
for why that's a second, separate task. Multi-dialect models: this creates one prog_object row per
enabled rdbms_type for the same tab_id — expect (and plan to fill) all of them, not just one.
Business Logic → Functionality → Control procedures → New (control_proc entity):
code_grp_id = VIEWS
control_proc_type = program_object_item (the code is a fragment woven into the generated
CREATE VIEW object, not a standalone object)
assign_type = static (one view = one hand-picked assignment; SQL/dynamic assignment is for
framework-internal or genuinely repeating patterns, not a one-off view query)
- ID: match the view name, or the model's chosen prefix convention (above). Multi-dialect: either
one
control_proc with one control_proc_template/template_id per platform, or a distinct
control_proc per platform — pick one convention and hold to it across the model, same as any
other naming decision here.
Add a control_proc_template, but leave template_code blank at first and regenerate — this
surfaces the real scaffold instead of guessing at it. Caveat, verified live: template_code
can be enforced as mandatory by the write API in use, rejecting a true empty string — if so, use a
short placeholder (e.g. -- placeholder) to get past the check, then overwrite it with the real
SELECT once the scaffold has been seen. Then write the full SELECT, in the
dialect(s) confirmed in step 1:
- Column list matches the view's modeled columns, in order, aliased to the exact
col_ids.
- Grep a sibling view's template in the same model — and the same
rdbms_type, if multi-dialect —
for join style, alias conventions, paging syntax (limit vs. top), date/time functions, and
comment-header style before introducing a new one — consistency beats individual preference (see
the style-continuity rule in the control-procedures skill).
- Follow the general Thinkwise SQL guidelines (see the control-procedures skill): lowercase
keywords, explicit column lists, no
SELECT *, comment non-obvious joins/filters, avoid
DISTINCT where a GROUP BY does the same job more explicitly.
-- Example shape, adapted from a real production view template (PostgreSQL dialect)
select c.customer_code
,cd.company_no
,c.customer_short_name
,c.customer_group
,cd.customer_type_id
from customer c
join customer_detail cd
on cd.customer_code = c.customer_code
where c.active = 1
Wire the template into the generated view object. Two equivalent routes (same mechanism as
any other static assignment — see the control-procedures skill's "Static assignment via API"
section for the full entity/field breakdown):
- UI: Functionality → Assigning tab → find the view's program object → attach the template.
- API/dynamic: insert a
template_prog_object_item row — prog_object_id = 'view_<tab_id>',
control_proc_id/template_id = the new control procedure/template, order_no = 10 (leaves
room to insert more fragments later, e.g. a second comment block or a UNION branch).
Multi-dialect: one such row per rdbms_type, each pointing prog_object_id's rdbms_type
at the matching dialect-specific template_id from step 5 — never point two platforms' rows at
the same template.
Generate the actual code — two distinct tasks, don't conflate them. See the
thinkwise_software_factory_create_control_procedures skill's "Actually generating code" section
for the full generate-code-group vs. generate-object-code walkthrough, including the
placeholder/re-run gotcha (a brand-new static assignment needs task_generate_code_grp re-run
against your control procedure, not just the framework one from step 3, or the generated body
silently keeps only the wrapper) — that mechanic is generic to any program_object_item control
procedure, not specific to views. What's specific to views:
- The generated program object is always named
view_<tab_id>. Target
task_add_job_to_generate_object_code's prog_object_code key at
(model_id, branch_id, rdbms_type, prog_object_id = 'view_<tab_id>').
- After generation, re-read the
prog_object row and sanity-check
prog_object_generated_code against intent (column list, joins, filters match what was modeled)
and against the dialect confirmed in step 1 (right date/time functions, paging clause,
identifier quoting) — a clean generate says nothing about dialect correctness by itself. If the
view replaces an ad hoc query (below), diff the generated SQL's logic against that original query
directly.
- Multi-dialect: repeat generation and the read-back for every
rdbms_type in
branch_rdbms_type, not just the first one that works. Query
/prog_object?$filter=... and tab_id eq '<tab>'&$select=rdbms_type,generated_code_stale and
confirm generated_code_stale = false for all of them before considering the view done.
- This is as far as this skill goes — deploying the generated code to a database is a separate step
outside its scope, and
prog_object isn't directly writable through this API either — don't
assume a successful generate means deploy is also possible; confirm with the user first.
Validate / code review as normal — move development_status through review, attach unit
tests if the view feeds anything business-critical (a report, a financial calculation, a process
flow decision).
Recursive/hierarchical (explosion) views
For a self-referencing hierarchy (a bill-of-materials explosion, an org chart, a category tree), the
Template SELECT is almost always a recursive CTE — this brings SQL-Server-specific gotchas around
query hints and type matching between the anchor and recursive members. See
references/recursive_views.md for the full detail before writing one.
Converting a user's ad hoc query into a view
Use when a user hands over a working SQL query (from SSMS, a report tool, a BI dashboard) and asks
for it to become a proper view. See references/adhoc_query_conversion.md for the full workflow —
it follows "Confirm scope before modeling", "Setting up a view — the basics", and "Writing and
assigning code to a view" above step-for-step, with a handful of conversion-specific differences
(deriving grain/PK from the query, confirming source names against the live model, diffing the
result against the original query).
Pre-flight checklist
- Query
branch_rdbms_type before writing a single line of the SELECT — one row, one dialect;
more than one row, one dialect-specific template + one template_prog_object_item per rdbms_type,
and verify generation for every one of them, not just the first that works. Verified live: writing
the wrong dialect (T-SQL against a PostgreSQL-only model) commits and "generates" without any error
at any step — the mistake only shows up by reading the actual generated SQL text.
- Model columns (order, PK, domains) before writing the
SELECT — the template must match the
column list, not the reverse.
- Default to
create_view_method = template unless the pull is genuinely a trivial single/few-table
join with no filter or aggregation.
- Set the view's own
tab.icon_id to a suitable icon (the view is a tab row like any table) — a
view built as a business work queue should get the work concept (e.g. "orders to release" →
order/check), not a generic view/database icon. Follow thinkwise_software_factory_icons.
- Reuse the source column's exact
dom_id for every view column that isn't a genuinely new derived
concept.
- Verify the chosen primary key is actually unique per row against real data — a view enforces
nothing on its own.
- Model a
ref/ref_col for every FK-shaped column before writing the template — never leave it as
"just a column with matching values." Remember the direction reversal for views: source_tab_id =
the real table, target_tab_id = the view (opposite of a normal table-to-table FK). Getting this
backwards renders as a detail grid on the view instead of the look-up you want.
- New view not showing up to attach code to? Run Generate code group (
task_generate_code_grp,
bound to control_proc) for VIEWS first — but that only creates the placeholder prog_object,
it doesn't produce code by itself.
- Leave a new template's code empty and generate the code group first — don't hand-guess the
scaffold. If the write API rejects a true-empty
template_code as mandatory, use a short
placeholder comment instead of blank.
- After wiring a brand-new
template_prog_object_item assignment, re-run task_generate_code_grp
bound to your own control procedure (not the framework one used to bootstrap the placeholder)
before generating — otherwise generation can report success while the view body silently contains
only the framework's wrapper fragments, missing your own SELECT. Read the generated code back and
confirm your own query is actually present, not just that the status was "Successful".
- Actually producing/refreshing the view's SQL is a second, separate task —
task_add_job_to_generate_object_code bound to prog_object_code. Confirm it actually ran via the
generate_object_code entity (generate_object_code_status = 3/"Successful", sorted
$orderby=job_id desc) and prog_object.generated_code_stale = false, not just a committed: true
from the task commit.
prog_object is not directly writable (403 on stage_resource add) — never try to hand-create or
patch it; always go through task_generate_code_grp.
- Generating code stays inside the model (
prog_object_generated_code) — it is not the same as
deploying to a live database, and this connector may not have rights for that separate step. Don't
offer to execute/deploy without checking with the user first.
- Grep a sibling view's template for join/alias/comment style before introducing a new convention.
- When converting a user's query: confirm every source table/column against the live model (don't
trust the raw SQL's names), and diff the view's output against the original query as the
acceptance test.
- Pick one control-procedure naming convention for views (bare match vs. a fixed prefix) and hold to
it across the model.
- The view and its columns are translatable objects with the same bracket-placeholder default as any
table/column — fill them in via
thinkwise_software_factory_translation_objects once the column
list is stable. Before calling the view done, run the translation completeness gate from
thinkwise_datamodeling_guidelines's "Translating new objects" section — don't rely on
remembering to translate each column as it's created; a real session translated the view itself and
still left every column at its bracket-placeholder text.
- Writing a recursive/self-referencing view? Read "Recursive/hierarchical (explosion) views"
above first — SQL Server rejects
OPTION (MAXRECURSION n) inside a view body outright, and
requires exact type matches (cast domain-backed columns, and re-cast accumulating arithmetic every
step) between the CTE's anchor and recursive members.
1---2name: thinkwise-software-factory-create-view3description: Reference guide for creating views in a Thinkwise Software Factory data model — naming, entity/column setup, domain reuse, modeling references to the view's source tables, and writing/assigning the SELECT code that backs a view. Use whenever creating, reviewing, or troubleshooting a view via an MCP connector with Software Factory access (e.g. sf_mcp, indicium), especially when converting a user's ad hoc SQL query into a view. Does not cover deployment.4---56# Creating Views in the Thinkwise Software Factory78Reference for the view lifecycle covered by this skill: `tab` (type `view`) → columns → references9→ control procedure → template → `template_prog_object_item` → generated `CREATE VIEW` program10object, generated and verified. Deployment (pushing the generated code to an actual database) is a11separate concern this skill does not cover.12Apply this whenever an MCP connector with Software Factory access (`sf_mcp`, `indicium`) is used to13create, inspect, or convert a query into a view — follow the connector's standard discovery→act flow;14never guess entity/task/property names. `tab`/`col` typically live in a `data_modeling`-style domain;15`control_proc`/`control_proc_template`/`template_prog_object_item` typically live in a16`manage_model`-style domain — try these directly first, and only escalate to17`search_capabilities`/`get_available_domains` on an `entity_set_not_found`/`domain_not_found`-style18rejection rather than re-discovering a domain that already resolved earlier this session. For19the general control-procedure/template/assignment mechanics referenced throughout (code groups,20`[PARMTR]` substitution, static vs. SQL assignment, dialect differences), see the21`thinkwise_software_factory_create_control_procedures` skill — this skill only covers what's specific22to views.2324## What a view is2526A view is a `tab` entity with `type_of_table = view` (enum: `table` 0, `view` 1, `function` 2,27`mqt` 4). Like a table it has columns, a primary key, references, screens, tasks, security —28everything a table has — except the data is never stored; it's composed at query time from a29`SELECT` statement, so it's always current and never needs syncing.3031`tab.create_view_method` picks how that `SELECT` gets defined (enum: `meta_auto` 0, `meta_custom` 1,32`template` 2):3334| Method | What you write | When to use |35|---|---|---|36| **Meta Auto** (`meta_auto`) | Nothing — map each view column to a source column (`col.view_tab_id`/`col.view_col_id`), run the `task_generate_view_from_clause` bound task, the `FROM`/join logic (`tab.view_from_clause`) is derived automatically | Only a straight pull from one or a few tables joined on their modeled references, no filtering, no aggregation |37| **Meta Custom** (`meta_custom`) | Hand-edit `tab.view_from_clause`/`view_where_clause`/`view_grp_by_clause`/`view_having_clause`; the `SELECT` clause is still derived from the view's columns | Rare middle ground — custom joins/filters but still want the platform to build the select list |38| **Template** (`template`) | The entire `SELECT` statement, written as a control procedure template | Anything with real business logic: multi-table joins, aggregation, calculated fields, `UNION`, conditional logic |3940**Default to Template.** In a large, mature production model inspected directly (210 views), every41single one used `create_view_method = template` — zero used Meta Auto or Meta Custom. A view earning42its keep almost always needs a join condition, a filter, or a derived column the auto-generated43`FROM` clause can't express, so teams converge on Template as the standard rather than starting with44Meta Auto and migrating later.4546A fourth `type_of_table` value, `mqt` (materialized query table / snapshot), persists and47periodically refreshes the result instead of computing it live. Reach for it only once a view's48underlying query becomes a measurable, repeated performance cost — don't start there pre-emptively.4950## Naming5152Base rules match tables generally: singular, self-explanatory, underscore-separated, no53abbreviations, no meta-information baked into the name. View-specific points:5455- **Name the view for the business question it answers, not its mechanics** — `customer_list`,56 `additional_cost`, not `join_customer_and_customer_detail`.57- **Reserve a prefix for platform-generated/dynamic-model view categories, used consistently.** The58 Thinkwise report-label pattern is the canonical example: any view feeding translated report labels59 is named with the `rpt_lbl_` prefix so the dynamic-model concept can find it. If a model has its60 own generated-view categories (audit/history, staging, …), give each one fixed prefix and document61 it — don't let it drift per developer.62- **Current official guidance is lowercase `snake_case`** (`customer_list`, not `Customer_List`).63 At least one large production model instead uses `PascalCase_With_Underscores` throughout (a64 legacy convention predating current guidelines). Follow whatever the model already uses; for a new65 model, follow the lowercase guideline. Consistency within one model matters more than which style66 is picked.67- **Name the control procedure and template after the view** so the code producing a view's data is68 trivially discoverable from the view's name. A bare match (`control_proc_id == tab_id`) is69 simplest and needs no explaining — pick one convention (bare match, or a fixed prefix like `Vw_`)70 and hold to it across the model; don't mix both.7172## Confirm scope before modeling7374Before creating the `tab` row, state the plan back to the user and get their confirmation — this75applies to every view being designed, not only an ad hoc-query conversion (see below). This is the76`thinkwise_software_factory_mcp_base` "Confirm-before-mutate" convention applied at this skill's own77grain; it isn't superseded by anything below about the view's own setup mechanics:7879- **Source tables/columns** the view pulls from.80- **Intended grain** — what one row of the view is meant to represent.81- **Primary-key candidate** — the column(s) that define that grain.82- **`create_view_method`** — Template vs. Meta Auto vs. Meta Custom (default to Template — see83 above).8485**Flag, don't silently decide, if the request is a poor fit for a plain view** — heavy aggregation86over a large table, queried by dozens of other things, on source data that barely changes. A87snapshot (`type_of_table = mqt`) or a scheduled denormalized table is often the better trade; raise88it with the user rather than defaulting to a view.8990## Setting up a view — the basics91921. **Data model → Tables → New table.** Set `type_of_table = view`, pick `create_view_method`93 (default `template`).942. **Model the columns before writing any code.** For Template views, the control procedure's95 `SELECT` list must match the view's modeled column list, in the same order — model columns first,96 then write `SELECT` to match, not the other way around.97 - **Column order**: same 10-per-step convention as regular tables (`col.order_no` 10, 20, 30, …),98 leaving gaps for later insertions.99 - **Primary key**: pick the column(s) that define the view's actual **grain** (the level of100 detail one row represents) — usually the query's `GROUP BY`/`DISTINCT` columns, or the source101 table's PK for a row-for-row pull. If the grain isn't obvious from the request, ask the user what102 one row of the view is meant to represent before picking the columns/primary key. Getting the103 grain wrong (a PK that isn't actually unique per row) is the most common view bug — nothing104 enforces uniqueness on a view by default, so verify manually against real data105 (`GROUP BY <candidate PK> HAVING COUNT(*) > 1`) before shipping.106 - **Reuse existing domains** for every column representing the same concept as an existing column107 elsewhere in the model — see next section.1083. **References are part of modeling the view, not an optional follow-up.** For every column on the109 view that is FK-shaped (its value is meant to match another table's primary key — e.g. an110 `employee_id` column, or an `absence_id`-style pointer), add the `ref`/`ref_col` at this point,111 before writing the query. They are never implied by the view's `FROM` clause; Meta Auto/Meta112 Custom infer references from mapped source columns, but Template gives none for free — skip this113 step on a Template view and the column silently has no look-up, no detail grid, and no114 auto-derived OData/Indicium navigation, even though the generated `SELECT` still returns the right115 data.116 - See the `thinkwise_datamodeling_guidelines` skill's "References (foreign keys)" section for the117 full look-up-vs-detail explanation. The one point specific to views, repeated here because it's118 easy to get backwards: **the direction is reversed from a normal table-to-table FK.** For a view119 column pointing at a real table's primary key, `source_tab_id` = the real table, `target_tab_id`120 = the view — not the other way around. Getting this backwards makes the reference render as a121 detail grid on the view instead of the look-up you almost always want there.122123 Example — view `employee_absence_overview`, column `employee_id` pointing at `employee`:124 ```125 ref: source_tab_id = employee, target_tab_id = employee_absence_overview,126 check_ref = false, show_detail = false127 ref_col: source_col_id = employee_id, target_col_id = employee_id128 ```129 `check_ref = false` because a view carries no physical FK constraint; `show_detail = false`130 because a detail grid on the real table listing view rows is rarely useful. If two or more view131 columns point at the same target table, set `ref_add` on each (e.g. `"most_recent"` /132 `"upcoming"`) so their auto-derived `ref_id`s don't collide.1334. Write and assign the actual query (below).1345. Regenerate and verify the generated code (below).135136**The new view and its columns need translating, same as any table.** `tab`/`col` created here get137the platform's usual bracket-placeholder translation until filled in — see138`thinkwise_software_factory_translation_objects` for the detection query and field reference. Do139this after the column list is finalized (step 2) so it isn't repeated every time a column gets140renamed while the view is still being modeled.141142## Reusing columns and domains143144A view column is not "read-only, so it doesn't matter" — every domain decision made for the source145tables should carry through:146147- **Match the domain (`col.dom_id`) of the source column exactly**, not just the data type. If148 `customer.customer_code` uses domain `Customer_Code`, the corresponding view column surfacing the149 customer code should use the same `Customer_Code` domain — not a fresh domain with the same150 `NVARCHAR(20)` shape. This keeps default UI controls, translations, input constraints, and151 domain-level validation consistent everywhere the value appears. Verified in a real production152 view: `Customer_Code`, `Customer_Group`, `Currency`, `Variety_No` and other columns all reuse the153 exact same domain IDs as their source tables; generic domains like a date domain or a datetime154 domain get reused wherever a date/datetime is surfaced, rather than each view minting its own.155- **Only introduce a new domain for a genuinely new business concept** — a computed/derived value156 with no 1:1 existing column (a calculated total, a concatenated display string, a derived flag).157 Don't create a "view-only" duplicate of an existing domain out of convenience.158- **Aggregations/calculations should still reuse the base domain where the semantics match** (a159 summed amount column can use the same currency/amount domain as the column being summed) — only160 diverge if the aggregation changes the meaning (a `COUNT` result is a plain integer, not whatever161 domain the counted column used).162- **FK-shaped columns inside a view keep the same column name as the table they reference**, exactly163 like normal FK columns — this is what lets Indicium/OData auto-derive the relationship and keeps164 look-ups working without extra configuration.165166## Writing and assigning code to a view (Template method)167168This is the verified, concrete object chain behind every Template view in a production model, traced169end-to-end via the Software Factory metadata API:170171`tab` (view) → generation produces a structural `prog_object` named `view_<tab_id>`, owned by the172framework's `VIEWS`-code-group meta control procedure → your own `control_proc` supplies the actual173`SELECT` as a fragment woven into that `prog_object` via a `template_prog_object_item` row.1741751. **Query `branch_rdbms_type`** (`/branch_rdbms_type?$filter=model_id eq '<model>' and branch_id eq176 '<branch>'` — see the control-procedures skill's "Check `branch_rdbms_type` first" section) before177 writing anything. One row → write the view's `SELECT` in that platform's dialect. More than one row178 → this view needs one dialect-specific `control_proc_template` per platform and one179 `template_prog_object_item` per `(rdbms_type, prog_object_id)` — decide this now, it changes steps 3180 and 5 below, not just the SQL text in step 4. Skipping this is exactly how a view ends up with181 `getdate()`/`dateadd`/`select top n` generated against a PostgreSQL-only model, or `limit`/`age()`182 generated against a SQL-Server-only one — every write along the way still reports success either183 way, so this has to be checked up front, not diagnosed after the fact.1842. **Model the view's columns first** (above) — order, primary key, domains. The `SELECT` written in185 step 5 must produce exactly this column list, in this order, with matching aliases.1863. **Make sure the view's structural program object exists.** A brand-new view has no generated187 program objects yet. Run **Generate code group** for the `VIEWS` code group (`task_generate_code_grp`,188 bound to `control_proc`, addressable by just `(model_id, branch_id, control_proc_id)` — use any189 control procedure already in `VIEWS`, e.g. the framework's own `pg_views`, since your own control190 procedure for this view doesn't exist yet at this point) once so the framework's `view_<tab_id>`191 program object shell exists — nothing can be attached to it before that. **This only creates the192 placeholder row — it does not generate any code yet**; see step 6 and the193 `thinkwise_software_factory_create_control_procedures` skill's "Actually generating code" section194 for why that's a second, separate task. Multi-dialect models: this creates one `prog_object` row per195 enabled `rdbms_type` for the same `tab_id` — expect (and plan to fill) all of them, not just one.1964. **Business Logic → Functionality → Control procedures → New** (`control_proc` entity):197 - `code_grp_id = VIEWS`198 - `control_proc_type = program_object_item` (the code is a *fragment* woven into the generated199 `CREATE VIEW` object, not a standalone object)200 - `assign_type = static` (one view = one hand-picked assignment; SQL/dynamic assignment is for201 framework-internal or genuinely repeating patterns, not a one-off view query)202 - ID: match the view name, or the model's chosen prefix convention (above). Multi-dialect: either203 one `control_proc` with one `control_proc_template`/`template_id` per platform, or a distinct204 `control_proc` per platform — pick one convention and hold to it across the model, same as any205 other naming decision here.2065. **Add a `control_proc_template`, but leave `template_code` blank at first** and regenerate — this207 surfaces the real scaffold instead of guessing at it. **Caveat, verified live**: `template_code`208 can be enforced as mandatory by the write API in use, rejecting a true empty string — if so, use a209 short placeholder (e.g. `-- placeholder`) to get past the check, then overwrite it with the real210 `SELECT` once the scaffold has been seen. Then write the full `SELECT`, **in the211 dialect(s) confirmed in step 1**:212 - Column list matches the view's modeled columns, in order, aliased to the exact `col_id`s.213 - Grep a sibling view's template in the same model — and the same `rdbms_type`, if multi-dialect —214 for join style, alias conventions, paging syntax (`limit` vs. `top`), date/time functions, and215 comment-header style before introducing a new one — consistency beats individual preference (see216 the style-continuity rule in the control-procedures skill).217 - Follow the general Thinkwise SQL guidelines (see the control-procedures skill): lowercase218 keywords, explicit column lists, no `SELECT *`, comment non-obvious joins/filters, avoid219 `DISTINCT` where a `GROUP BY` does the same job more explicitly.220221 ```sql222 -- Example shape, adapted from a real production view template (PostgreSQL dialect)223 select c.customer_code224 ,cd.company_no225 ,c.customer_short_name226 ,c.customer_group227 ,cd.customer_type_id228 from customer c229 join customer_detail cd230 on cd.customer_code = c.customer_code231 where c.active = 1232 ```2336. **Wire the template into the generated view object.** Two equivalent routes (same mechanism as234 any other static assignment — see the control-procedures skill's "Static assignment via API"235 section for the full entity/field breakdown):236 - **UI**: Functionality → Assigning tab → find the view's program object → attach the template.237 - **API/dynamic**: insert a `template_prog_object_item` row — `prog_object_id = 'view_<tab_id>'`,238 `control_proc_id`/`template_id` = the new control procedure/template, `order_no = 10` (leaves239 room to insert more fragments later, e.g. a second comment block or a `UNION` branch).240 Multi-dialect: one such row **per `rdbms_type`**, each pointing `prog_object_id`'s `rdbms_type`241 at the matching dialect-specific `template_id` from step 5 — never point two platforms' rows at242 the same template.2437. **Generate the actual code — two distinct tasks, don't conflate them.** See the244 `thinkwise_software_factory_create_control_procedures` skill's "Actually generating code" section245 for the full generate-code-group vs. generate-object-code walkthrough, including the246 placeholder/re-run gotcha (a brand-new static assignment needs `task_generate_code_grp` re-run247 against *your* control procedure, not just the framework one from step 3, or the generated body248 silently keeps only the wrapper) — that mechanic is generic to any `program_object_item` control249 procedure, not specific to views. What's specific to views:250 - The generated program object is always named `view_<tab_id>`. Target251 `task_add_job_to_generate_object_code`'s `prog_object_code` key at252 `(model_id, branch_id, rdbms_type, prog_object_id = 'view_<tab_id>')`.253 - After generation, re-read the `prog_object` row and sanity-check254 `prog_object_generated_code` against intent (column list, joins, filters match what was modeled)255 **and against the dialect confirmed in step 1** (right date/time functions, paging clause,256 identifier quoting) — a clean generate says nothing about dialect correctness by itself. If the257 view replaces an ad hoc query (below), diff the generated SQL's logic against that original query258 directly.259 - **Multi-dialect: repeat generation and the read-back for every `rdbms_type` in260 `branch_rdbms_type`**, not just the first one that works. Query261 `/prog_object?$filter=... and tab_id eq '<tab>'&$select=rdbms_type,generated_code_stale` and262 confirm `generated_code_stale = false` for all of them before considering the view done.263 - This is as far as this skill goes — deploying the generated code to a database is a separate step264 outside its scope, and `prog_object` isn't directly writable through this API either — don't265 assume a successful generate means deploy is also possible; confirm with the user first.2668. **Validate / code review as normal** — move `development_status` through review, attach unit267 tests if the view feeds anything business-critical (a report, a financial calculation, a process268 flow decision).269270## Recursive/hierarchical (explosion) views271272For a self-referencing hierarchy (a bill-of-materials explosion, an org chart, a category tree), the273Template `SELECT` is almost always a recursive CTE — this brings SQL-Server-specific gotchas around274query hints and type matching between the anchor and recursive members. See275`references/recursive_views.md` for the full detail before writing one.276277## Converting a user's ad hoc query into a view278279Use when a user hands over a working SQL query (from SSMS, a report tool, a BI dashboard) and asks280for it to become a proper view. See `references/adhoc_query_conversion.md` for the full workflow —281it follows "Confirm scope before modeling", "Setting up a view — the basics", and "Writing and282assigning code to a view" above step-for-step, with a handful of conversion-specific differences283(deriving grain/PK from the query, confirming source names against the live model, diffing the284result against the original query).285286## Pre-flight checklist287288- **Query `branch_rdbms_type` before writing a single line of the `SELECT`** — one row, one dialect;289 more than one row, one dialect-specific template + one `template_prog_object_item` per `rdbms_type`,290 and verify generation for every one of them, not just the first that works. Verified live: writing291 the wrong dialect (T-SQL against a PostgreSQL-only model) commits and "generates" without any error292 at any step — the mistake only shows up by reading the actual generated SQL text.293- Model columns (order, PK, domains) before writing the `SELECT` — the template must match the294 column list, not the reverse.295- Default to `create_view_method = template` unless the pull is genuinely a trivial single/few-table296 join with no filter or aggregation.297- Set the view's own `tab.icon_id` to a suitable icon (the view is a `tab` row like any table) — a298 view built as a business work queue should get the work concept (e.g. "orders to release" →299 order/check), not a generic view/database icon. Follow `thinkwise_software_factory_icons`.300- Reuse the source column's exact `dom_id` for every view column that isn't a genuinely new derived301 concept.302- Verify the chosen primary key is actually unique per row against real data — a view enforces303 nothing on its own.304- Model a `ref`/`ref_col` for every FK-shaped column before writing the template — never leave it as305 "just a column with matching values." Remember the direction reversal for views: `source_tab_id` =306 the real table, `target_tab_id` = the view (opposite of a normal table-to-table FK). Getting this307 backwards renders as a detail grid on the view instead of the look-up you want.308- New view not showing up to attach code to? Run **Generate code group** (`task_generate_code_grp`,309 bound to `control_proc`) for `VIEWS` first — but that only creates the placeholder `prog_object`,310 it doesn't produce code by itself.311- Leave a new template's code empty and generate the code group first — don't hand-guess the312 scaffold. If the write API rejects a true-empty `template_code` as mandatory, use a short313 placeholder comment instead of blank.314- **After wiring a brand-new `template_prog_object_item` assignment, re-run `task_generate_code_grp`315 bound to your own control procedure (not the framework one used to bootstrap the placeholder)316 before generating** — otherwise generation can report success while the view body silently contains317 only the framework's wrapper fragments, missing your own `SELECT`. Read the generated code back and318 confirm your own query is actually present, not just that the status was "Successful".319- Actually producing/refreshing the view's SQL is a **second**, separate task —320 `task_add_job_to_generate_object_code` bound to `prog_object_code`. Confirm it actually ran via the321 `generate_object_code` entity (`generate_object_code_status = 3`/`"Successful"`, sorted322 `$orderby=job_id desc`) and `prog_object.generated_code_stale = false`, not just a `committed: true`323 from the task commit.324- `prog_object` is not directly writable (`403` on `stage_resource` add) — never try to hand-create or325 patch it; always go through `task_generate_code_grp`.326- Generating code stays inside the model (`prog_object_generated_code`) — it is not the same as327 deploying to a live database, and this connector may not have rights for that separate step. Don't328 offer to execute/deploy without checking with the user first.329- Grep a sibling view's template for join/alias/comment style before introducing a new convention.330- When converting a user's query: confirm every source table/column against the live model (don't331 trust the raw SQL's names), and diff the view's output against the original query as the332 acceptance test.333- Pick one control-procedure naming convention for views (bare match vs. a fixed prefix) and hold to334 it across the model.335- The view and its columns are translatable objects with the same bracket-placeholder default as any336 table/column — fill them in via `thinkwise_software_factory_translation_objects` once the column337 list is stable. **Before calling the view done, run the translation completeness gate from338 `thinkwise_datamodeling_guidelines`'s "Translating new objects" section** — don't rely on339 remembering to translate each column as it's created; a real session translated the view itself and340 still left every column at its bracket-placeholder text.341- **Writing a recursive/self-referencing view?** Read "Recursive/hierarchical (explosion) views"342 above first — SQL Server rejects `OPTION (MAXRECURSION n)` inside a view body outright, and343 requires exact type matches (cast domain-backed columns, and re-cast accumulating arithmetic every344 step) between the CTE's anchor and recursive members.