Literals
Literals are the constant values you write directly in queries — numbers, strings, dates, arrays. The way you write a value picks its type: 1 is a BIGINT (exact, integer family), 1.0 is a DECIMAL (exact, decimal family), and 1e0 is a DOUBLE (approximate, floating family). Category vs family, casts, and TYPEOF() are on Types ; this page is the notation.
Many typed literals (TYPE 'value') are validated at compile time so malformed values fail before SQL runs on your backend.
Numeric literals
Integer size, decimal precision, and floating-point type are all controlled by how you write the number:
RETURN
f_tinyint := TYPEOF(TINYINT '1'),
f_smallint := TYPEOF(SMALLINT '1'),
f_int := TYPEOF(INT '1'),
f_bigint := TYPEOF(1),
f_decimal := TYPEOF(1.123),
f_float := TYPEOF(FLOAT '1.1'),
f_double := TYPEOF(DOUBLE '1.1'),
f_doublesci := TYPEOF(1.1e0)
;| F_TINYINT VARCHAR | F_SMALLINT VARCHAR | F_INT VARCHAR | F_BIGINT VARCHAR | F_DECIMAL VARCHAR | F_FLOAT VARCHAR | F_DOUBLE VARCHAR | F_DOUBLESCI VARCHAR |
|---|---|---|---|---|---|---|---|
| TINYINT | SMALLINT | INT | BIGINT | DECIMAL(4,3) | FLOAT | DOUBLE | DOUBLE |
The TYPEOF() results reveal the inferred types. Plain 1 defaults to BIGINT. Decimal literals like 1.123 get automatic precision tracking (DECIMAL(4,3)). For floating point, use either the keyword prefix (FLOAT '1.1', DOUBLE '1.1') or scientific notation (1.1e0 → DOUBLE).
Untyped integer literals use the BIGINT range. Values outside -9223372036854775808 … 9223372036854775807 are rejected at compile time — there is no silent widening to a larger integer type.
Other scalar literals
Strings, booleans, dates, timestamps, intervals, JSON, and bitstrings each have their own syntax:
RETURN
f_varchar := TYPEOF('A'),
f_boolean := TYPEOF(TRUE),
f_date := TYPEOF(DATE '2024-10-15'),
f_timestamp := TYPEOF(TIMESTAMP '2024-10-15 10:04:00'),
f_interval := TYPEOF(INTERVAL '3 DAYS 1 MINUTE'),
f_json := TYPEOF(JSON '"A"'),
f_bitstring := TYPEOF(BITSTRING '1010')
;| F_VARCHAR VARCHAR | F_BOOLEAN VARCHAR | F_DATE VARCHAR | F_TIMESTAMP VARCHAR | F_INTERVAL VARCHAR | F_JSON VARCHAR | F_BITSTRING VARCHAR |
|---|---|---|---|---|---|---|
| VARCHAR | BOOLEAN | DATE | TIMESTAMP(3) | INTERVAL | JSON | BITSTRING |
Strings use single quotes only — 'A'. Double quotes wrap identifiers ("FM.FUNCTIONS.AREA"), not strings. Write booleans as TRUE or FALSE, not BOOLEAN 'true'. Temporal literals require a keyword prefix: DATE '2024-10-15', TIMESTAMP '2024-10-15 10:04:00'. Timestamps support up to microsecond precision: TIMESTAMP '2024-10-15 10:04:00.123456'. JSON literals wrap JSON content in single quotes after the JSON keyword. Bitstrings use BITSTRING '1010' where the payload is a non-empty string of 0 and 1 only.
Binary bytes use hex: X'0102' or BINARY '0102' (even-length hex). Geometry and geography use a subtype prefix plus WKT, for example GEOM_POINT 'POINT(0 0)' or GEOG_POINT 'POINT(-122.4 37.8)' — not bare GEOM '…'. Per-type examples and backend availability: All types .
Use plain 'text' for strings and TRUE/FALSE for booleans. VARCHAR 'hello' and BOOLEAN 'true' are rejected with a short message pointing you to the preferred syntax.
Typed literal validation
When you write TYPE 'value', FeatureQL checks the string payload against rules for that type. Invalid literals produce a compile-time error with a specific code (for example UE/LITERAL-DATE-INVALID-FORMAT) instead of failing later in SQL or returning a surprising coerced value.
| Family | Typed prefix | What is checked |
|---|---|---|
| Integer | TINYINT, SMALLINT, INT, BIGINT | Integer syntax, no fraction or exponent, per-type range |
| Decimal | DECIMAL | Numeric syntax (at most one .) |
| Floating | FLOAT, DOUBLE | Numeric syntax, including exponent |
| Temporal | DATE, TIMESTAMP, TIMESTAMPTZ | Calendar date or date-time string shape; DATE must not include a time part |
| Other scalars | INTERVAL, JSON, BITSTRING, BINARY | Interval text, JSON, bit digits 0/1, or even-length hex |
| Geospatial | GEOM_*, GEOG_* | Non-empty WKT whose leading keyword matches the subtype |
| Meta | TYPE, FEATURE, FIELD | Type/feature/field name resolution (not a string payload check) |
A common mistake is putting a date-and-time string on a DATE literal. FeatureQL rejects that and tells you to use TIMESTAMP instead:
RETURN DATE '2015-06-03 15:56:23';Integer typed literals and untyped BIGINT literals share the same range checks. This fails at compile time because the value does not fit in BIGINT:
RETURN 123456789123456789123456789;BITSTRING literals accept only binary digits. Valid values compile to SQL as CAST('…' AS BITSTRING) on backends such as DuckDB:
RETURN
(BITSTRING '1010')::VARCHAR
;| ?_0 VARCHAR |
|---|
| 1010 |
Invalid payloads are caught immediately:
RETURN BITSTRING '10a1';Complex type literals
Arrays and rows let you build structured data inline:
RETURN
f_array := TYPEOF(ARRAY(1, 2, 3)),
f_row := TYPEOF(ROW(1, 'A')),
f_array_of_rows := TYPEOF(ARRAY(ROW(1 AS col1, 'A' AS col2)))
;| F_ARRAY VARCHAR | F_ROW VARCHAR | F_ARRAY_OF_ROWS VARCHAR |
|---|---|---|
| ARRAY<BIGINT> | ROW<field_1 BIGINT, field_2 VARCHAR> | ARRAY<ROW<col1 BIGINT, col2 VARCHAR>> |
ARRAY[1,2,3] creates a homogeneous list. ROW(1, 'A') creates a struct with auto-generated field names. Add AS to name the fields: ROW(1 as col1, 'A' as col2). Nest them — ARRAY[ROW(...)] — to create the array-of-rows structure that is central to FeatureQL's data model.
Special values
FeatureQL has two kinds of "nothing" — NULL and EMPTY() — and they behave differently:
RETURN
EMPTY(),
TYPEOF(EMPTY()),
NULL::VARCHAR,
TYPEOF(NULL),
NULL()::VARCHAR,
TYPEOF(NULL()),
NULL::BIGINT::VARCHAR,
TYPEOF(NULL::DOUBLE)
;| ?_0 VARCHAR | ?_1 VARCHAR | ?_2 VARCHAR | ?_3 VARCHAR | ?_4 VARCHAR | ?_5 VARCHAR | ?_6 VARCHAR | ?_7 VARCHAR |
|---|---|---|---|---|---|---|---|
| EMPTY() | EMPTY | NULL | UNSPECIFIED | NULL | UNSPECIFIED | NULL | DOUBLE |
NULL works like SQL's NULL: it represents a missing value and can be cast to any type. NULL without a type is UNSPECIFIED; NULL(DOUBLE) creates a typed null directly. NULL(DOUBLE) and NULL::DOUBLE are equivalent — both produce a typed null.
EMPTY() is different — it represents an explicitly empty value used for optional function parameters. Unlike NULL, EMPTY() cannot be cast to another type. If you try EMPTY()::BIGINT, you get an error.
NULL in comparisons
Comparisons use three-valued logic: NULL = NULL is unknown (NULL), not TRUE. Nested NULL inside a ROW or ARRAY poisons the whole atomic comparison the same way — there is no lexicographic early-exit past a missing field. For null-safe equality, write IS NOT DISTINCT FROM (NULL IS NOT DISTINCT FROM NULL is TRUE). DISPLAYS AS is the permissive spelling of the same test.
RETURN
eq_null := NULL::BIGINT = NULL::BIGINT,
da_null := NULL::BIGINT IS NOT DISTINCT FROM NULL::BIGINT,
eq_row := ROW(1::BIGINT, NULL::BIGINT) = ROW(1::BIGINT, NULL::BIGINT),
da_row := ROW(1::BIGINT, NULL::BIGINT)
IS NOT DISTINCT FROM ROW(1::BIGINT, NULL::BIGINT)
;| EQ_NULL BOOLEAN | DA_NULL BOOLEAN | EQ_ROW BOOLEAN | DA_ROW BOOLEAN |
|---|---|---|---|
| NULL | TRUE | NULL | TRUE |
GROUP BY, ARRAY_DISTINCT, and ORDER BY follow their own key/sort rules (not the same as < / =). See also All operators and the comparison function pages.
Next
Operators & functions — + vs ADD(), constants, SHOW FUNCTIONS.