بنقرة واحدة
migrate-from-cube
// Migrate a Cube.js semantic layer to airlayer .view.yml files. Use when the user has existing Cube.js schema files (.js or .yml) they want to convert to airlayer format.
// Migrate a Cube.js semantic layer to airlayer .view.yml files. Use when the user has existing Cube.js schema files (.js or .yml) they want to convert to airlayer format.
Run a semantic query against the database via airlayer. Use when the user wants to query data through the semantic layer, test view definitions, or debug query results.
Bootstrap a semantic layer from a database. Use when the user wants to create .view.yml files from their warehouse schema, or when starting a new airlayer project from scratch.
Profile dimensions in the semantic layer to discover data values, ranges, and cardinality. Use when the user wants to understand what data is in a dimension, find valid filter values, or validate view definitions against actual data.
Inspect semantic layer views, dimensions, measures, and entities. Use when the user wants to explore what's available in their .view.yml files or understand the schema structure.
Compile a semantic query to SQL using airlayer. Use when the user wants to generate SQL from .view.yml schemas, test a query against their semantic layer, or translate dimensions/measures/filters into dialect-specific SQL.
Validate .view.yml semantic layer files using airlayer. Use when the user creates or modifies view files and wants to check for errors.
| name | migrate-from-cube |
| description | Migrate a Cube.js semantic layer to airlayer .view.yml files. Use when the user has existing Cube.js schema files (.js or .yml) they want to convert to airlayer format. |
You are converting Cube.js schema files into airlayer .view.yml files. Each Cube.js cube becomes one .view.yml file.
Cube.js schemas can be in two formats. Read all the cube files and identify which format they use:
JavaScript format (.js files in schema/ or model/cubes/):
cube(`orders`, {
sql_table: `public.orders`,
dimensions: { ... },
measures: { ... },
joins: { ... },
})
YAML format (.yml files in model/cubes/):
cubes:
- name: orders
sql_table: public.orders
dimensions: ...
measures: ...
joins: ...
YAML vs JS naming: Cube.js YAML uses camelCase measure types (
countDistinct,countDistinctApprox,avg) while JS uses snake_case (count_distinct,avg). Both mean the same thing — map them identically.
Read ALL cube files before starting to write any .view.yml — you need the full picture to correctly translate joins into entity pairs.
Joins are the most important thing to get right. airlayer uses entities on both sides of a join — the entity name must match on both views for airlayer to auto-generate the JOIN.
For each join in the Cube.js schema, parse the join SQL to find the FK and PK columns:
Cube.js join (declared on the FK side / "owning" cube):
joins: {
customers: {
relationship: `many_to_one`,
sql: `${CUBE}.customer_id = ${customers}.id`,
},
},
Translation:
customer_id) → foreign entity on ordersid) → primary entity on customerscustomer)# orders.view.yml
entities:
- name: customer
type: foreign
key: customer_id
# customers.view.yml
entities:
- name: customer
type: primary
key: id
Entity naming rules:
customers → entity name customer)one_to_many joins (the owning side has the PK): entity is primary on the owning cube, foreign on the targetmany_to_one joins (the owning side has the FK): entity is foreign on the owning cube, primary on the targetcustomer ≠ CustomerCritical: key/keys must reference dimension names, not column names.
airlayer validates that entity keys match the name: of a dimension in the same view — NOT the raw column name in expr:. If your dimension is named store_id with expr: Store, the entity key must be store_id, not Store.
# WRONG — 'Store' is a column name, not a dimension name
entities:
- name: store
type: primary
keys: ["Store", "Date"] # ✗ validator error
# CORRECT — use the dimension names defined in the same view
entities:
- name: store
type: primary
keys: ["store_id", "week_date"] # ✓ matches dimension name fields
# For single-column keys, use key: (not keys:)
entities:
- name: store
type: primary
key: store_id # ✓
Build a table mapping (cube_name, entity_name) → (type, key) for all joins before writing any files.
For each cube, create a .view.yml file in the project directory.
sql_table: `public.orders` → table: public.orders
sql: `SELECT * FROM orders WHERE active = true` → table: "(SELECT * FROM orders WHERE active = true)"
For cubes using sql: (a subquery), wrap the value in quotes as the table: value.
Type mapping:
| Cube.js type | airlayer type | Notes |
|---|---|---|
string | string | |
number | number | |
boolean | boolean | |
time | date | Use datetime only if the column includes a time component (e.g., timestamps) |
geo | string | Note the approximation in a comment |
Expression rewriting:
sql: \column_name`→expr: column_name`sql: \${CUBE}.column_name`→expr: column_name(strip${CUBE}.`)sql: \${CUBE}.qty * ${CUBE}.price`→expr: "qty * price"(strip all${CUBE}.` prefixes)sql: \${OtherCube}.column`→ **cannot go inexpr`** directly; see cross-view references belowPrimary key dimensions:
primary_key: true → declare as a dimension AND add a primary entityExample — JS:
dimensions: {
order_id: { sql: `id`, type: `number`, primary_key: true },
status: { sql: `status`, type: `string` },
created_at: { sql: `created_at`, type: `time` },
line_total: { sql: `${CUBE}.qty * ${CUBE}.unit_price`, type: `number` },
}
→ YAML:
entities:
- name: order
type: primary
key: id
dimensions:
- name: order_id
type: number
expr: id
- name: status
type: string
expr: status
- name: created_at
type: datetime
expr: created_at
- name: line_total
type: number
expr: "qty * unit_price"
Type mapping:
| Cube.js type | airlayer type | Notes |
|---|---|---|
count | count | No expr needed |
count_distinct | count_distinct | |
count_distinct_approx | count_distinct | Becomes exact; note the change |
sum | sum | |
avg | average | Cube.js uses avg, airlayer uses average |
min | min | |
max | max | |
number | number | Computed/derived — use expr with the formula (see note below) |
running_total | (no equivalent) | Use the cumulative motif at query time instead |
number vs custom — picking the right type:
type: number when the expression contains row-level math (no aggregate function) and airlayer wraps it: expr: "qty * unit_price"type: custom when the expression is already a full aggregation and should be passed through verbatim: VARIANCE(Weekly_Sales), CORR(Temperature, Sales), STDDEV(x) / AVG(x) * 100# number — row-level formula, airlayer adds no wrapper
- name: profit_margin
type: number
expr: "total_profit / NULLIF(total_revenue, 0)"
# custom — expression already contains its own aggregate function(s)
- name: sales_stddev
type: custom
expr: "STDDEV(Weekly_Sales)"
- name: temp_correlation
type: custom
expr: "CORR(Temperature, Weekly_Sales)"
Measure filters:
completed_count: {
type: `count`,
filters: [{ sql: `${CUBE}.status = 'completed'` }],
}
→
- name: completed_count
type: count
filters:
- expr: "status = 'completed'"
Filter expression rewriting rules:
${CUBE}.column = 'value' → strip ${CUBE}. → column = 'value'column = 'value' (no ${CUBE}. prefix) → use as-is → column = 'value'Derived measures (type: number):
profit_margin: {
sql: `${total_profit} / NULLIF(${total_revenue}, 0)`,
type: `number`,
}
→
- name: profit_margin
type: number
expr: "total_profit / NULLIF(total_revenue, 0)"
Replace ${measure_name} references with the measure name directly (airlayer resolves measure refs by name).
segments: {
completed: { sql: `${CUBE}.status = 'completed'` },
high_value: { sql: `${CUBE}.amount > 1000` },
}
→
segments:
- name: completed
expr: "status = 'completed'"
- name: high_value
expr: "amount > 1000"
When a dimension sql references another cube — ${OtherCube}.column — it's a subquery dimension. Use sub_query: true:
// In customers cube
total_orders: {
sql: `${orders.count}`,
type: `number`,
sub_query: true,
}
→
- name: total_orders
type: number
expr: "orders.order_count"
sub_query: true
The expr for a sub_query dimension is view_name.measure_name referencing the measure from the related view.
Every .view.yml must have:
name: <cube_name> # snake_case
table: <sql_table value> # actual DB table/schema.table
Optional but recommended:
description: "..." # from cube's title/description field if present
dialect: postgres # match the database type
datasource: warehouse # match the database name from config.yml
After writing all .view.yml files, validate them:
airlayer validate
Fix any errors reported. Common issues:
expr on a measure that requires oneFor any views connected by entities, test a cross-view query to confirm joins work:
airlayer query \
--dimension customers.name \
--measure orders.total_revenue
If the join fails, check that entity names match exactly across both views.
The following Cube.js features have no equivalent in airlayer. Skip them during migration:
| Cube.js feature | Action |
|---|---|
pre_aggregations | Skip — airlayer has no caching layer |
refresh_key | Skip — caching directive |
rolling_window on measures | Use moving_average or cumulative motif at query time |
access_policy / public: false | Skip — airlayer has no row-level security |
data_source override per cube | Set datasource: at the view level instead |
extends (cube inheritance) | Inline the inherited fields manually |
shown: false on members | Skip — airlayer shows all members |
Input (orders.js):
cube(`orders`, {
sql_table: `orders`,
joins: {
customers: {
relationship: `many_to_one`,
sql: `${CUBE}.customer_id = ${customers}.id`,
},
},
dimensions: {
order_id: { sql: `id`, type: `number`, primary_key: true },
status: { sql: `status`, type: `string` },
created_at: { sql: `created_at`, type: `time` },
},
measures: {
count: { type: `count` },
total_revenue: { sql: `amount`, type: `sum` },
avg_order_value: { sql: `amount`, type: `avg` },
completed_count: {
type: `count`,
filters: [{ sql: `${CUBE}.status = 'completed'` }],
},
},
});
Output (orders.view.yml):
name: orders
table: orders
entities:
- name: order
type: primary
key: id
- name: customer
type: foreign
key: customer_id
dimensions:
- name: order_id
type: number
expr: id
- name: status
type: string
expr: status
- name: created_at
type: datetime
expr: created_at
measures:
- name: count
type: count
- name: total_revenue
type: sum
expr: amount
- name: avg_order_value
type: average
expr: amount
- name: completed_count
type: count
filters:
- expr: "status = 'completed'"
Output (customers.view.yml):
name: customers
table: customers
entities:
- name: customer
type: primary
key: id
dimensions:
- name: id
type: number
expr: id
# ... other dimensions