Skip to content

Identifiers

TesseraQL has one rule for names: the column name is the name. The identifier you write in DDL is the same string you write as the YAML input name, the 2-way SQL bind, the URL path parameter, the suite parameter, the template model key, and the JSON response key. There is no case-conversion layer — nothing turns order_date into orderDate between the database and the browser, so every layer can be grepped with one string.

A table, column, alias, or field name is one Unicode letter or underscore followed by Unicode letters, combining marks, digits, or underscores:

identifier = [letter or _] [letter, combining mark, digit, or _]*
qualified name = identifier [ "." identifier ]

That includes Japanese — 受注, 顧客名, 受注番号 are names like any other — and every other script: the marks are what an abugida needs, so ग्राहक and มุ่งมั่น are names too. A name typed in decomposed (NFC vs NFD) form is the same kind of name, and nothing normalizes it — no database does either, so a name spelled one way works and a name spelled two ways never matches. The linter reports that pair as TQL-SQL-2121 rather than letting it bind null. It excludes anything that could read as SQL syntax: spaces, quotes, dashes, semicolons, comment markers. Identifiers land in generated SQL verbatim and unquoted, and this character class is what makes that safe.

A params: key is a bind name, so it is an identifier too, and the linter reports one that is not with TQL-SQL-2120. The rule is not pedantry: a bind name is an expression the directive parses, so params: { order-id: query.order-id } with a matching /* order-id */ bind reads as the subtraction order - id, binds null on every request, and says nothing.

A complete Japanese example ships in the gallery: examples/juchu-kanri-app defines the 受注 table, routes at /受注 and /受注/{受注番号}, a 送料区分 decision, and a suite that exercises them with Japanese parameter names.

-- The bind name is the column name; the JSON key will be too.
select 受注番号, 顧客名, 状態
from 受注
where 受注番号 = /* 受注番号 */'J-1001'

Names that address infrastructure rather than data keep the narrow ASCII shapes their targets require: app names (they become URL prefixes, directories, and the per-app migration-history table suffix), topic and environment-profile names, preference keys, DuckDB extension and secret names, Prometheus label names, and SCIM attributes.

Every supported dialect accepts unquoted Unicode identifiers, and because CJK has no letter case, the engines’ case-folding differences cannot touch them. Two practical limits to know:

Engine Identifier length limit
PostgreSQL 63 bytes (about 21 kanji in UTF-8) — the tightest
Oracle 128 bytes from 12.2 (30 bytes before)
MySQL 64 characters
SQL Server 128 characters

Length is not linted — the database’s own error is authoritative. On Oracle the database character set must be Unicode (AL32UTF8, the modern default) for non-ASCII identifiers. One adjacent note about values rather than names: on SQL Server, store Japanese text in nvarchar columns — TesseraQL always sends values as bind parameters (which the driver transmits as Unicode), but a hand-written plain '…' literal in your own SQL tooling would pass through the database code page.

Browsers percent-encode non-ASCII URLs. The runtime decodes exactly the non-ASCII percent-sequences of a request path before route matching, so GET /%E5%8F%97%E6%B3%A8 reaches the route declared at web/受注/. ASCII sequences such as %2F deliberately stay encoded — decoding them would let an encoded slash cross a path-segment boundary. Path parameters keep their declared names throughout your app and the OpenAPI document; the HTTP router internally carries positional stand-ins for names it cannot represent, and the request binder maps them back before your SQL sees anything.

Studio’s documentation search indexes Japanese identifiers so that any substring run finds them — 管理 finds 受注管理. Message-catalog placeholders ({顧客名}), view link templates, and workflow stamp columns all accept the same identifier contract, so a Japanese app gets the same lint coverage — including the write-scope guard — as an ASCII one.