본문 바로가기
WIKI 기술 지식 베이스

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.
`uuid`, `json`, `jsonb` 타입은 해당 확장이 컴파일되어 있어야 해요. 표준 Turso 빌드에는 기본으로 포함되어 있어요.

복합 타입: 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으로 타입을 삭제할 수 없어요.
STRICT가 아닌 테이블이 당시 존재하지 않던 타입 이름으로 만들어졌고(예: `CREATE TABLE t(x mytype)`), 그 후에 같은 이름의 커스텀 타입이 만들어졌다면(`CREATE TYPE mytype ...`), 그 타입의 ENCODE/DECODE 로직은 기존 테이블에 소급 적용되지 **않아요**. 이건 버그가 아니에요: STRICT가 아닌 테이블은 일치하는 커스텀 타입이 존재하는지와 무관하게 모든 타입 이름을 그저 어피니티 힌트로 취급해요. STRICT 테이블은 CREATE TABLE 시점에 알 수 없는 타입 이름을 거절하기 때문에 이런 상황을 예방해요.
  • 표현식 안 서브쿼리 없음: 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)