CREATE TYPE
CREATE TYPE는 값을 저장 전에 어떻게 인코딩하고, 읽을 때 어떻게 디코딩하고, 입력을 어떻게 검증하고, 정렬 비교를 어떻게 하고, 기본값은 무엇을 쓸지 제어하는 사용자 정의 타입을 정의해요.
출처: 문서
본문
**Turso 확장 기능**: CREATE TYPE은 표준 SQLite에는 없는 Turso 전용 문이에요. 커스텀 타입은 STRICT 테이블이 필요해요. 이 기능은 실험적이며 사용 전에 [활성화](/sql-reference/experimental-features)해야 해요.CREATE TYPE 문은 사용자 정의 타입을 정의해요. 이 타입은 값이 저장 전에 어떻게 인코딩되고, 읽을 때 어떻게 디코딩되고, 입력에서 어떻게 검증되고, 정렬을 위해 어떻게 비교되고, 어떤 기본값을 쓸지 제어해요. 커스텀 타입은 STRICT 테이블 타입 시스템을 SQLite 내장 저장 클래스 다섯 개 너머로 확장해요.
문법
CREATE TYPE [IF NOT EXISTS] type-name [(parameters)]
BASE base-type
ENCODE encode-expr
DECODE decode-expr
[OPERATOR 'op' [function-name] ...]
[DEFAULT default-expr];
설명
커스텀 타입은 네 가지 기본 저장 타입 중 하나를 사용자 정의 로직으로 감싸요. 커스텀 타입 컬럼에 값을 쓰면 ENCODE 표현식이 저장 전에 변환해요. 값을 읽으면 DECODE 표현식이 되돌려줘요. 덕분에 쿼리에는 다른 모양을 보여주면서 효율적인 디스크 표현으로 데이터를 저장할 수 있어요.
커스텀 타입은 STRICT 테이블에서만 동작해요. STRICT가 아닌 테이블에서 커스텀 타입 이름을 써도 효과가 없어요.
절
IF NOT EXISTS
같은 이름의 타입이 이미 존재할 때 생길 오류를 억제해요. 기존 타입은 그대로 유지돼요.
CREATE TYPE IF NOT EXISTS cents BASE integer ENCODE value * 100 DECODE value / 100;
BASE
값을 디스크에 저장할 때 쓰이는 기반 SQLite 저장 클래스를 지정해요. 모든 커스텀 타입은 BASE 절이 필요해요.
| 기본 타입 | 설명 |
|---|---|
integer |
부호 있는 정수. 크기에 따라 1-8바이트로 저장돼요 |
real |
8바이트 IEEE 754 부동소수점 수 |
text |
UTF-8 인코딩 문자열 |
blob |
원시 이진 데이터 |
CREATE TYPE cents BASE integer
ENCODE value * 100
DECODE value / 100;
ENCODE / DECODE
ENCODE 표현식은 이 타입 컬럼에 값을 쓸 때마다 평가돼요. DECODE 표현식은 값을 읽을 때마다 평가돼요. 두 표현식 모두 입력을 가리킬 때 식별자 value를 써요.
- ENCODE: 입력 값을 기본 저장 형태로 변형해요. INSERT, UPDATE, CAST에서 실행돼요.
- DECODE: 저장된 값을 다시 표현 형태로 변형해요. SELECT에서 실행돼요.
- NULL 처리: NULL 값은 ENCODE와 DECODE를 모두 우회해요. NULL 입력은 표현식을 평가하지 않고 NULL 출력을 만들어요.
-- Store text in reversed form
CREATE TYPE reversed_text BASE text
ENCODE turso_reverse(value)
DECODE turso_reverse(value);
ENCODE 표현식이 검증 로직을 담는 자리예요. ENCODE 표현식이 오류를 일으키면 INSERT나 UPDATE가 중단돼요.
매개변수
커스텀 타입은 ENCODE와 DECODE 표현식에서 쓸 수 있는 매개변수를 받을 수 있어요. 매개변수는 타입 이름 뒤의 괄호 목록으로 선언해요. 각 매개변수는 이름과 선택적 타입 주석을 가져요.
CREATE TYPE varchar(value text, maxlen integer) BASE text
ENCODE CASE
WHEN length(value) <= maxlen THEN value
ELSE RAISE(ABORT, 'value too long for varchar')
END
DECODE value;
컬럼이 매개변수 타입을 쓰면 인수는 타입 이름 뒤의 괄호 안에 넣어요:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name varchar(100),
bio varchar(500)
) STRICT;
첫 번째 매개변수는 항상 입력 value예요. 추가 매개변수는 컬럼 타입 선언에서 순서대로 제공된 인수들이에요.
OPERATOR
OPERATOR 절은 SQL 연산자를 함수에 매핑해요. 덕분에 커스텀 타입 컬럼이 비교, 산술, 정렬에 참여할 수 있어요.
OPERATOR 'op' function-name
| 구성 요소 | 설명 |
|---|---|
'op' |
문자열 리터럴로 쓴 연산자 기호: '+', '-', '*', '/', '<', '=' 등 |
function-name |
이 연산자가 쓰일 때 호출할 SQL 함수 이름. 생략하면 기본 타입의 고유 비교를 써요. |
연산자가 정의되면 column + 1 같은 표현식은 function_name(column, 1)으로 다시 쓰여요.
OPERATOR '<'로 정렬하기
< 연산자는 특별해요: 이 타입의 값이 ORDER BY, MIN, MAX, CREATE INDEX에서 어떻게 정렬되는지 결정해요.
OPERATOR '<'(함수 이름 없음): 정렬에 기본 타입의 고유 비교를 써요. 인코딩된 형태가 올바르게 정렬되는 타입(예: ISO 8601 형식의 텍스트 날짜)에는 이걸로 충분해요.OPERATOR '<' my_compare:my_compare(a, b)를 커스텀 비교자로 써요. 이 함수는a < b면 음수,a = b면 0,a > b면 양수를 반환해야 해요.<연산자 없음: 이 타입은 ORDER BY나 CREATE INDEX에서 쓸 수 없어요.
-- Use base type comparison for ordering
CREATE TYPE cents BASE integer
ENCODE value * 100
DECODE value / 100
OPERATOR '<';
-- Full operator set for a numeric type
CREATE TYPE uint BASE integer
ENCODE CASE WHEN value >= 0 THEN value ELSE RAISE(ABORT, 'uint must be non-negative') END
DECODE value
OPERATOR '+' uint_add
OPERATOR '-' uint_sub
OPERATOR '*' uint_mul
OPERATOR '<'
OPERATOR '=';
= 연산자는 정의되어 있으면 !=를 유도하는 데도 쓰여요. < 연산자는 인수를 바꾸거나 결과를 뒤집어서 >, >=, <=를 자동으로 유도해요.
DEFAULT
DEFAULT 절은 타입 수준의 기본값을 설정해요. 이 타입의 컬럼이 자체 DEFAULT를 지정하지 않으면 이 값이 쓰여요.
CREATE TYPE boolean BASE integer
ENCODE boolean_to_int(value)
DECODE CASE WHEN value THEN 1 ELSE 0 END
OPERATOR '<'
DEFAULT 0;
컬럼 수준의 DEFAULT가 타입 수준의 DEFAULT를 이겨요:
CREATE TABLE flags (
id INTEGER PRIMARY KEY,
is_active boolean, -- uses type default (0)
is_admin boolean DEFAULT 1 -- overrides type default
) STRICT;
RAISE로 검증하기
ENCODE 표현식에서 CASE/WHEN과 RAISE를 써서 쓰기 시점에 입력을 검증하세요. 조건이 실패하면 지정한 오류 메시지와 함께 문이 중단돼요.
CREATE TYPE positive_int BASE integer
ENCODE CASE
WHEN value > 0 THEN value
ELSE RAISE(ABORT, 'value must be positive')
END
DECODE value;
CREATE TYPE email BASE text
ENCODE CASE
WHEN value LIKE '%_@_%.__%' THEN lower(value)
ELSE RAISE(ABORT, 'invalid email address')
END
DECODE value;
CAST 지원
CAST 표현식은 커스텀 타입의 ENCODE 로직을 적용해요:
CREATE TYPE cents BASE integer
ENCODE value * 100
DECODE value / 100;
SELECT CAST(42.5 AS cents);
-- 4250
이건 INSERT/UPDATE 밖에서, 예를 들어 WHERE 절이나 CHECK 제약에서, 값을 인코딩된 형태로 바꿀 때 유용해요.
커스텀 타입과 CHECK 제약
커스텀 타입을 쓰는 STRICT 테이블에서 CHECK 제약은 디코딩된(표현) 값에 대해 동작해요. 커스텀 타입 컬럼을 리터럴과 비교할 때는 CAST로 리터럴을 인코딩하세요:
CREATE TYPE cents BASE integer
ENCODE value * 100
DECODE value / 100;
CREATE TABLE products (
id INTEGER PRIMARY KEY,
price cents CHECK(price >= CAST(0 AS cents))
) STRICT;
타입 들여다보기
PRAGMA list_types
내장과 사용자 정의를 포함해 사용 가능한 모든 타입을 나열해요:
PRAGMA list_types;
-- name
-- boolean
-- varchar
-- date
-- time
-- timestamp
-- smallint
-- numeric
-- ...
sqlite_turso_types 가상 테이블
sqlite_turso_types 가상 테이블은 각 타입의 이름과 SQL 정의를 제공해요:
SELECT name, sql FROM sqlite_turso_types;
-- name | sql
-- boolean | CREATE TYPE boolean(value any) BASE integer ENCODE ...
-- ...
ALTER TABLE과 함께 쓰기
ALTER TABLE ADD COLUMN으로 컬럼을 추가할 때 커스텀 타입을 쓸 수 있어요:
ALTER TABLE users ADD COLUMN email varchar(255);
추가된 컬럼은 원래 CREATE TABLE에서 정의된 컬럼과 같은 STRICT 타입 검사 규칙을 따라요.
내장 타입
Turso는 STRICT 테이블에서 다음 내장 커스텀 타입을 제공해요:
| 타입 | 기본 타입 | 매개변수 | 설명 |
|---|---|---|---|
boolean |
integer | -- | 0 또는 1로 제한. 별칭: bool. |
smallint |
integer | -- | -32768부터 32767까지로 제한된 정수. 별칭: int2. |
bigint |
integer | -- | 정수 (범위 제약 없음). 별칭: int8. |
varchar(N) |
text | maxlen |
최대 N 문자 길이의 텍스트. |
date |
text | -- | ISO 8601 날짜(YYYY-MM-DD). 삽입 시 검증돼요. |
time |
text | -- | ISO 8601 시각(HH:MM:SS). 삽입 시 검증돼요. |
timestamp |
text | -- | ISO 8601 날짜·시각. 삽입 시 검증돼요. |
numeric(P,S) |
blob | precision, scale |
P자릿수 전체와 S자릿수 소수를 가진 고정소수점 10진수. 산술 연산자를 지원해요. |
uuid |
blob | -- | 16바이트 blob으로 저장되고 문자열로 표시되는 UUID. 기본값: uuid4_str(). |
inet |
text | -- | 검증된 IP 주소 (IPv4 또는 IPv6). |
bytea |
blob | -- | 이진 데이터 (PostgreSQL 호환 별칭). |
json |
text | -- | 검증된 JSON 텍스트. |
jsonb |
blob | -- | 이진 형식으로 저장되고 읽을 때 JSON 텍스트로 반환되는 JSON. |
복합 타입: STRUCT와 UNION
인코딩/디코딩 커스텀 타입 외에, CREATE TYPE은 두 가지 복합 타입 형태를 지원해요: STRUCT(이름 붙은 곱 타입)와 UNION(판별 유니언 / 태그 붙은 변형). 둘 다 데이터를 디스크에는 blob으로 저장하고 STRICT 테이블이 필요해요.
STRUCT
STRUCT는 이름 붙은 여러 필드를 하나의 컬럼으로 묶어요. 개별 필드를 읽고 필터링할 때는 점 표기법(dot notation)을 쓰세요:
CREATE TYPE point AS STRUCT(x INT, y INT);
CREATE TABLE locations (
id INTEGER PRIMARY KEY,
pos point
) STRICT;
INSERT INTO locations VALUES (1, struct_pack(10, 20));
INSERT INTO locations VALUES (2, struct_pack(30, 40));
-- Read fields with dot notation
SELECT pos.x, pos.y FROM locations WHERE id = 1;
-- 10|20
-- Filter and order by fields
SELECT id, pos.x FROM locations WHERE pos.y > 25 ORDER BY pos.x;
-- 2|30
점 표기법은 컬럼 표현식이 들어가는 어디에서든 동작해요 — SELECT, WHERE, ORDER BY, GROUP BY, HAVING, 집계 함수:
CREATE TYPE address AS STRUCT(street TEXT, city TEXT, zip TEXT);
CREATE TABLE people(name TEXT, home address) STRICT;
INSERT INTO people VALUES ('Alice', struct_pack('123 Main St', 'Springfield', '62704'));
INSERT INTO people VALUES ('Bob', struct_pack('456 Oak Ave', 'Springfield', '62701'));
SELECT home.city, COUNT(*) FROM people GROUP BY home.city;
-- Springfield|2
테이블 이름과 컬럼 이름이 부딪히면 테이블 참조가 언제나 이겨요. struct 필드에 접근하려면 별칭을 쓰세요:
-- t.x here is table.column, not column.field
SELECT t.x FROM t;
-- Use an alias to access the struct field
SELECT s.pos.x FROM locations AS s;
struct_pack()
INSERT와 UPDATE용 struct 값을 만들어요. 인수는 위치 기반이고, 타입 정의의 필드 순서와 대응돼요.
INSERT INTO locations VALUES (1, struct_pack(10, 20));
UPDATE locations SET pos = struct_pack(99, 88) WHERE id = 1;
struct_extract()
점 표기법의 함수 형태예요 — struct_extract(col, 'field')는 col.field와 동일해요. 주로 점 표기법을 쓸 수 없는 표현식 인덱스에서 유용해요:
CREATE INDEX idx_x ON locations(struct_extract(pos, 'x'));
UNION
UNION은 판별 유니언(태그 붙은 변형)이에요. 각 값은 선언된 변형 중 정확히 하나를 태그로 식별되어 담고 있어요. 점 표기법으로 변형의 값을 꺼내면, 활성 변형이 일치하지 않을 때 NULL을 반환해요:
CREATE TYPE platform_id AS UNION(telegram INT, slack TEXT, signal TEXT);
CREATE TABLE contacts (
id INTEGER PRIMARY KEY,
platform platform_id
) STRICT;
INSERT INTO contacts VALUES (1, union_value('telegram', 12345));
INSERT INTO contacts VALUES (2, union_value('slack', 'U0ABC'));
INSERT INTO contacts VALUES (3, union_value('signal', '+1555'));
-- Dot notation extracts the variant value (NULL if tag doesn't match)
SELECT id, platform.telegram, platform.slack FROM contacts ORDER BY id;
-- 1|12345|
-- 2||U0ABC
-- 3||
-- Combine with union_tag() to filter by variant
SELECT id, platform.slack FROM contacts WHERE union_tag(platform) = 'slack';
-- 2|U0ABC
변형이 struct 타입인 유니언에서는 점 접근을 체인처럼 이어서 중첩 필드에 도달해요:
CREATE TYPE telegram_msg AS STRUCT(chat_id INT, text TEXT);
CREATE TYPE msg AS UNION(telegram telegram_msg, slack TEXT);
CREATE TABLE messages(id INT, data msg) STRICT;
INSERT INTO messages VALUES (1, union_value('telegram', struct_pack(100, 'hello')));
-- col.variant.field
SELECT data.telegram.chat_id, data.telegram.text FROM messages;
-- 100|hello
union_value()
INSERT와 UPDATE용 유니언 값을 만들어요. 첫 번째 인수는 변형 태그(문자열 리터럴)이고, 두 번째는 값이에요. 태그는 대상 컬럼의 유니언 타입에 맞춰 해석돼요.
INSERT INTO contacts VALUES (1, union_value('telegram', 12345));
UPDATE contacts SET platform = union_value('slack', 'U0XYZ') WHERE id = 1;
union_tag()
활성 변형의 태그 이름을 텍스트로 반환해요.
SELECT id, union_tag(platform) FROM contacts ORDER BY id;
-- 1|telegram
-- 2|slack
-- 3|signal
union_extract()
점 표기법의 함수 형태예요 — union_extract(col, 'variant')는 col.variant와 동일해요. 주로 표현식 인덱스에서 유용해요:
CREATE INDEX idx_tg ON contacts(union_extract(platform, 'telegram'));
NULL 처리
NULL 값은 모든 struct와 union 연산에서 전파돼요:
CREATE TYPE number AS UNION(i INT, f REAL);
CREATE TABLE values_t(id INT, val number) STRICT;
INSERT INTO values_t VALUES (1, union_value('i', 42));
INSERT INTO values_t VALUES (2, union_value('f', 3.14));
INSERT INTO values_t VALUES (3, NULL);
SELECT id, val.i, val.f FROM values_t ORDER BY id;
-- 1|42|
-- 2||3.14
-- 3||
점 표기법 우선순위
점 표현식 a.b가 모호할 때 (예: a가 테이블 이름일 수도 컬럼 이름일 수도), 테이블 참조가 struct 필드 접근보다 항상 우선해요. 이건 DuckDB의 해석 규칙을 따르는 거예요.
CREATE TYPE point AS STRUCT(x INT, y INT);
CREATE TABLE t(x INT, t point) STRICT;
INSERT INTO t VALUES (99, struct_pack(10, 20));
-- t.x resolves as table.column (99), not column.field (10)
SELECT t.x FROM t;
-- 99
-- Use an alias to reach the struct field
SELECT s.t.x FROM t AS s;
-- 10
a.b.c의 해석 순서는: database.table.column, 그 다음 table.column.field예요.
인라인 타입은 지원되지 않음
컬럼 정의 안에 인라인 STRUCT/UNION 선언은 지원되지 않아요. 반드시 먼저 CREATE TYPE을 쓰세요:
-- This will error:
CREATE TABLE t(data STRUCT(x INT, y INT)) STRICT;
-- Do this instead:
CREATE TYPE point AS STRUCT(x INT, y INT);
CREATE TABLE t(data point) STRICT;
배열 타입
배열 컬럼은 값들의 순서 있는 컬렉션을 저장해요. STRUCT와 UNION과 달리, 배열은 명시적인 CREATE TYPE이 필요 없어요 — STRICT 테이블 컬럼 정의에서 아무 기본 타입 이름 뒤에 []를 붙여서 선언해요:
CREATE TABLE sensors (
id INTEGER PRIMARY KEY,
readings REAL[],
labels TEXT[],
flags INTEGER[]
) STRICT;
INSERT INTO sensors VALUES (1, ARRAY[1.5, 2.5, 3.5], ARRAY['temp','humidity'], '[0, 1, 1]');
SELECT readings[0], labels[1], array_length(flags) FROM sensors;
-- 1.5|humidity|3
다차원 배열은 대괄호 쌍을 여러 개 써요:
CREATE TABLE matrices (
id INTEGER PRIMARY KEY,
data INTEGER[][]
) STRICT;
INSERT INTO matrices VALUES (1, ARRAY[ARRAY[1,2], ARRAY[3,4]]);
SELECT data[1][0] FROM matrices;
-- 3
원소 타입은 삽입 시점에 검증돼요 — INTEGER[] 컬럼은 정수로 저장할 수 없는 값을 거절해요. 배열은 내부적으로는 압축된 레코드 형식 BLOB으로 저장되고, 출력 시에는 JSON 배열로 표시돼요.
배열 함수(array_agg, array_append, array_contains, array_slice 등), 연산자(@>, &&, ||), 첨자 문법의 전체 목록은 배열 함수를 참고하세요.
제한 사항
- STRICT 테이블 전용: STRICT가 아닌 테이블에서는 커스텀 타입 이름이 무시돼요. 컬럼은 대신 표준 타입 어피니티 규칙을 따라요.
- 사용 중일 때 삭제 불가: 어떤 테이블이 그 타입의 컬럼을 가지고 있는 동안에는 DROP TYPE으로 타입을 삭제할 수 없어요.
- 표현식 안 서브쿼리 없음: ENCODE, DECODE, DEFAULT 표현식에는 서브쿼리, 집계 함수, 윈도우 함수가 들어갈 수 없어요.
- STRUCT/UNION 컬럼의 인덱스 없음: STRUCT나 UNION 컬럼 위의 CREATE INDEX는 지원되지 않아요.
예제
항등 타입 (통과)
변형 없이 값을 저장하고 꺼내는 최소한의 타입이에요:
CREATE TYPE my_text BASE text ENCODE value DECODE value;
CREATE TABLE notes (
id INTEGER PRIMARY KEY,
body my_text
) STRICT;
센트 단위의 금액 값
달러 금액을 정수 센트로 저장해 정확한 산술을 하고, 원래 값으로 보여줘요:
CREATE TYPE cents BASE integer
ENCODE value * 100
DECODE value / 100
OPERATOR '<';
CREATE TABLE invoices (
id INTEGER PRIMARY KEY,
amount cents
) STRICT;
INSERT INTO invoices VALUES (1, 42);
-- Stored as 4200 on disk, queried as 42
SELECT amount FROM invoices;
-- 42
JSON 검증
삽입된 값이 유효한 JSON인지 검증해요:
CREATE TYPE json BASE text
ENCODE json(value)
DECODE value;
CREATE TABLE configs (
id INTEGER PRIMARY KEY,
data json
) STRICT;
INSERT INTO configs VALUES (1, '{"key": "value"}');
-- succeeds
INSERT INTO configs VALUES (2, 'not json');
-- Error: malformed JSON
연산자가 있는 비부호 정수
산술과 비교 연산자를 갖춘 음수가 아닌 정수 타입이에요:
CREATE TYPE uint BASE integer
ENCODE CASE
WHEN value >= 0 THEN value
ELSE RAISE(ABORT, 'uint must be non-negative')
END
DECODE value
OPERATOR '<'
OPERATOR '=';
CREATE TABLE counters (
id INTEGER PRIMARY KEY,
count uint DEFAULT 0
) STRICT;
INSERT INTO counters VALUES (1, 5);
SELECT * FROM counters WHERE count > 0 ORDER BY count;
고정소수점 10진수
정밀한 10진수 연산에는 내장 numeric 타입을 쓰세요:
CREATE TABLE transactions (
id INTEGER PRIMARY KEY,
amount numeric(10, 2),
tax numeric(10, 2)
) STRICT;
INSERT INTO transactions VALUES (1, 99.99, 8.25);
SELECT amount + tax FROM transactions;
-- 108.24
더 알아보기 (Learn more)
- DROP TYPE - 커스텀 타입 삭제하기
- CREATE DOMAIN - ENCODE/DECODE 로직 없이 제약만 있는 타입 별칭 정의
- 데이터 타입 - 타입 시스템과 타입 어피니티 개요
- 배열 타입 - 네이티브 배열 컬럼 (
INTEGER[],TEXT[]등) - 배열 함수 - 배열 만들기, 조작, 연산자
- CREATE TABLE - STRICT 테이블 정의