Skip to content

Commit 7760940

Browse files
Document types and virtual sources experiment (#298)
Add new experiments page for type declarations and virtual sources (WN-0024). Update connections, sources, experiments list, and TOC with cross-references to the new page. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bec88c3 commit 7760940

5 files changed

Lines changed: 209 additions & 2 deletions

File tree

src/documentation/experiments/experiments.malloynb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Below is a list of currently running experiments and how to turn them on.
1111
* `##! experimental { function_order_by partition_by aggregate_limit }` - [ordering and partitioning in calculations](window.malloynb)
1212
* `##! experimental.sql_functions` - [Write expression in SQL](sql_expressions.malloynb)
1313
* `##! experimental.parameters` - [Declare sources with parameters](parameters.malloynb)
14-
* `##! experimental.composite_sources` - [Create virtual sources backed by multiple cube tables or source definitions](composite_sources.malloynb)
14+
* `##! experimental.composite_sources` - [Create composite sources backed by multiple cube tables or source definitions](composite_sources.malloynb)
1515
* `##! experimental.access_modifiers` - [Limit access to fields in a source](include.malloynb)
16-
* `##! experimental.persistence` - [Persist sources as tables with the CLI builder](persistence.malloynb)
16+
* `##! experimental.persistence` - [Persist sources as tables with the CLI builder](persistence.malloynb)
17+
* `##! experimental.virtual_source` - [Declare types and define virtual sources with no underlying table](virtual_sources.malloynb)
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
>>>markdown
2+
## Types and Virtual Sources
3+
4+
Malloy's `type:` declaration and `::` type operator introduce two related capabilities:
5+
6+
- **Type declarations on table sources** make a source self-describing — the schema lives in the `.malloy` file, not just the database. An LLM can reason about the source without a connection, a CI pipeline can validate without credentials, and a human can see the fields without running a query.
7+
8+
- **Virtual sources** define a source with no underlying table. The type declaration *is* the schema. The actual table is resolved at query time through a mapping, making it possible to write models against tables that don't exist yet or to point the same model at different tables in different environments.
9+
10+
This is an experimental feature. Enable it with `##! experimental.virtual_source` at the top of your `.malloy` file.
11+
12+
See [WN-0024 (Type Declarations and Virtual Sources)](https://github.com/malloydata/whatsnext/blob/main/wns/WN-0024-schema-and-virtual-sources/wn-0024.md) for the full design.
13+
14+
### Declaring Types
15+
16+
A `type:` declaration is a top-level statement, like `source:` or `query:`. It defines a named collection of typed fields:
17+
18+
```malloy
19+
##! experimental.virtual_source
20+
21+
type: store is {
22+
id :: number,
23+
name :: string,
24+
street :: string,
25+
city :: string,
26+
state :: string
27+
}
28+
```
29+
30+
A type has no connection to any database and no behavior on its own. It becomes meaningful when applied to a source.
31+
32+
### Supported Types
33+
34+
Type declarations support the full range of Malloy types:
35+
36+
| Type | Example |
37+
|------|---------|
38+
| Basic types | `x :: string`, `y :: number`, `z :: boolean`, `d :: date`, `t :: timestamp` |
39+
| Timestamp with timezone | `t :: timestamptz` |
40+
| Arrays | `tags :: string[]`, `scores :: number[]` |
41+
| Inline records | `address :: { street :: string, city :: string }` |
42+
| Named types | `location :: address` |
43+
| Arrays of records | `items :: { name :: string, qty :: number }[]` |
44+
| SQL native types | `big_id :: "BIGINT"`, `geo :: "GEOGRAPHY"` |
45+
46+
SQL native types are written as quoted strings, for database-specific types that don't have a Malloy equivalent.
47+
48+
### Composing Types
49+
50+
Types can reference other named types, allowing nested structures to be composed from reusable pieces:
51+
52+
```malloy
53+
type: address is {
54+
street :: string,
55+
city :: string,
56+
state :: string,
57+
zip :: string
58+
}
59+
60+
type: store is {
61+
id :: number,
62+
name :: string,
63+
location :: address
64+
}
65+
```
66+
67+
A type can also incorporate the fields of another type using `extend`:
68+
69+
```malloy
70+
type: cigar_store is store extend {
71+
humidor_capacity :: number,
72+
walk_in :: boolean
73+
}
74+
```
75+
76+
This produces a type with all the fields of `store` plus the new fields. There is no subtype relationship — the result is a flat bag of fields.
77+
78+
### Applying Types to Table Sources
79+
80+
The `::` type operator on a source expression declares the **complete expected schema** of that source:
81+
82+
```malloy
83+
##! experimental.virtual_source
84+
85+
type: airport_fields is {
86+
code :: string,
87+
city :: string,
88+
state :: string,
89+
elevation :: number
90+
}
91+
92+
source: airports is duckdb.table('airports.parquet')::airport_fields
93+
```
94+
95+
When applied to a table source, the type:
96+
97+
1. **Hides undeclared fields.** Columns not listed in the type are marked `hidden`. They still exist — joins and expressions can reference them — but they won't appear in query output or auto-complete.
98+
99+
2. **Validates types.** If a declared field exists but its type doesn't match the actual column, the compiler reports an error.
100+
101+
3. **Validates existence.** If a declared field doesn't exist in the table at all, the compiler reports an error.
102+
103+
Dimensions, measures, and joins defined in an `extend` block are unaffected — the type only governs intrinsic (table-derived) fields.
104+
105+
#### Multiple Types
106+
107+
You can apply several types at once with parentheses:
108+
109+
```malloy
110+
type: id_fields is { id :: number, name :: string }
111+
type: metric_fields is { revenue :: number, cost :: number }
112+
113+
source: report is duckdb.table('report.parquet')::(id_fields, metric_fields)
114+
```
115+
116+
This is equivalent to creating a single type that extends the others.
117+
118+
#### Narrowing a Source
119+
120+
Because `::` always means "the complete expected schema," you can narrow an existing source by applying a smaller type:
121+
122+
```malloy
123+
##! experimental.virtual_source
124+
125+
type: full_schema is { id :: number, name :: string, city :: string }
126+
type: narrow_schema is { id :: number, name :: string }
127+
128+
source: a is duckdb.virtual('x')::full_schema
129+
130+
// b has all of a's fields, but only id and name are public
131+
source: b is a::narrow_schema
132+
```
133+
134+
### Virtual Sources
135+
136+
A virtual source uses `connection.virtual('name')` instead of `connection.table('path')`. There is no underlying table — the type *defines* the fields:
137+
138+
```malloy
139+
##! experimental.virtual_source
140+
141+
type: user_facts_fields is {
142+
user_id :: string,
143+
signup_date :: date,
144+
lifetime_value :: number,
145+
segment :: string
146+
}
147+
148+
source: user_facts is duckdb.virtual('user_facts')::user_facts_fields
149+
```
150+
151+
Virtual sources can be extended like any other source:
152+
153+
```malloy
154+
source: user_facts_model is duckdb.virtual('user_facts')::user_facts_fields extend {
155+
dimension: signup_year is year(signup_date)
156+
measure: total_ltv is lifetime_value.sum()
157+
measure: user_count is count()
158+
}
159+
```
160+
161+
#### Resolving Virtual Sources at Query Time
162+
163+
At query time, the application provides a **virtual map** that resolves virtual names to actual table paths. For example, the same model can point at different tables in different environments:
164+
165+
```malloy
166+
##! experimental.virtual_source
167+
168+
type: feature_fields is { user_id :: string, feature_vec :: string }
169+
170+
// The model is the same everywhere
171+
source: features is bq.virtual('features')::feature_fields
172+
173+
// Dev: bq → features → "dev.features_sample"
174+
// Prod: bq → features → "prod.ml_features_v3"
175+
// Test: bq → features → "test_fixtures.features_small"
176+
```
177+
178+
If a virtual source is used in a query and no map entry exists, the compiler produces an error — there is no fallback, because a virtual source has no SQL of its own.
179+
180+
### Summary
181+
182+
| Source Type | Type Behavior | Fields Come From |
183+
|-------------|---------------|------------------|
184+
| `table()` without `::` | All table columns are public | Database |
185+
| `table()::type` | Declared columns public, rest hidden, types validated | Database (validated) |
186+
| `virtual()::type` | Declared columns are the only fields | Type declaration |
187+
| `virtual()` without `::` | Empty source (legal but useless) | Nothing |
188+
>>>markdown

src/documentation/language/connections.malloynb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ In Postgres, the string passed to the `.table()` connection method can be a two-
4141

4242
The `.sql()` connection method is used to define a source or query based on a SQL query. See the [SQL Sources](./sql_sources.malloynb) section for more information.
4343

44+
## Virtual Connection Method _(experimental)_
45+
46+
The `.virtual()` connection method defines a source with no underlying table. The string argument is a logical name that is resolved to an actual table at query time through a virtual map. Virtual sources require a `type:` declaration applied with the `::` operator to define their fields.
47+
48+
```malloy
49+
##! experimental.virtual_source
50+
51+
type: order_fields is { order_id :: string, order_date :: date, total :: number }
52+
source: orders is duckdb.virtual('orders')::order_fields
53+
```
54+
55+
See [Types and Virtual Sources](../experiments/virtual_sources.malloynb) for details. Enable with `##! experimental.virtual_source`.
56+
4457
---
4558

4659
## Configuring Connections

src/documentation/language/source.malloynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A source can be any of the following:
1212
| [A SQL table or SQL view](#sources-from-tables-or-views)| `duckdb.table('data/flights.parquet')` |
1313
| [A Malloy query](#sources-from-malloy-queries) | `flights -> { group_by: carrier }` |
1414
| [A SQL query](#sources-from-sql-queries) | `duckdb.sql("""select 1 as one""")` |
15+
| [A virtual source](../experiments/virtual_sources.malloynb) _(experimental)_ | `duckdb.virtual('name')::type` |
1516

1617
A source can be used directly in a query:
1718
>>>malloy

src/table_of_contents.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@
427427
{
428428
"title": "Persistence",
429429
"link": "/experiments/persistence.malloynb"
430+
},
431+
{
432+
"title": "Types and Virtual Sources",
433+
"link": "/experiments/virtual_sources.malloynb"
430434
}
431435
]
432436
}

0 commit comments

Comments
 (0)