SQL dialect
SQL dialect는 하나의 데이터베이스 시스템이 사용하는 SQL 문법과 실행 규칙이다. 이 프로젝트에서 mysql, postgres, sqlite는 identifier quoting, placeholder, 타입 변환, write 문법, 지원 SQL 함수를 선택한다. Planner는 database별 SQL 요소를 선택한 SQL dialect에 요청한다. IR과 plan 형식은 변경하지 않는다.
DIA-1 identifiers — MySQL 8.0.2+ / MariaDB 10.2+:
`x`; PostgreSQL 12+:"x"; SQLite 3.46+:"x"DIA-2 placeholders — MySQL 8.0.2+ / MariaDB 10.2+:
?; PostgreSQL 12+:$n(the executor renumbers when it expands aparentslot to N values); SQLite 3.46+:?DIA-3 LIMIT — MySQL 8.0.2+ / MariaDB 10.2+:
LIMIT off, n; PostgreSQL 12+:LIMIT n OFFSET off; SQLite 3.46+:LIMIT n OFFSET offDIA-4
Lk— MySQL 8.0.2+ / MariaDB 10.2+:F(col) LIKE F(?), F는A–Z각각을 소문자로 바꾸는REPLACE26겹: column의utf8mb4_0900_bin은 code point로 비교하고,LOWER나_cicollation은 ASCII 밖까지 접는다(probemysql.like.binary_collation_case_sensitive,mysql.like.ascii_fold_replace); PostgreSQL 12+:COLLATE "C"아래의ILIKE(probepostgres.ilike.c_collation_ascii_only); SQLite 3.46+:LIKE … ESCAPE '\'(ASCII 대소문자 구분 없음, probesqlite.like.ascii_only)DIA-5
Lb— MySQL 8.0.2+ / MariaDB 10.2+:LIKE(binary collationutf8mb4_0900_bin, code point로 비교한다.LIKE BINARY는 MySQL 8.0.27부터 deprecated다. probemysql.like.binary_collation_code_point); PostgreSQL 12+:LIKE; SQLite 3.46+:instr(col, ?) > 0DIA-6
Fulltext/FulltextBoolean— MySQL 8.0.2+ / MariaDB 10.2+:MATCH … AGAINST (… IN NATURAL LANGUAGE/BOOLEAN MODE), boolean value normalized by the transformfulltext_boolean(PRT-32): split the value on whitespace and drop empty terms; remove the characters+ - < > ( ) ~ * " @from each term and drop a term that becomes empty; prefix every term with+and suffix the last term with*; join the terms with single spaces; a value with no term fails withIR_INVALID; PostgreSQL 12+:to_tsvector('simple', coalesce(a,'') || ' ' || …) @@ plainto_tsquery / websearch_to_tsquery('simple', ?), value trimmed only; SQLite 3.46+: rejected (FTS5 needs virtual tables)DIA-7
forceIndex<Name>— MySQL 8.0.2+ / MariaDB 10.2+:FORCE INDEX (name); PostgreSQL 12+: ignored (no hints); SQLite 3.46+:INDEXED BY "name"DIA-8 insert id — MySQL 8.0.2+ / MariaDB 10.2+:
LAST_INSERT_ID()(upsert addspk = LAST_INSERT_ID(pk)); PostgreSQL 12+:RETURNING pk; SQLite 3.46+:RETURNING pkDIA-9
duplication(model)— MySQL 8.0.2+ / MariaDB 10.2+:ON DUPLICATE KEY UPDATE(validator가 허용한 충돌 key 하나만 만난다. MySQL은 아무 unique key에서나 행과 만나며(mysql.upsert.any_unique_key), NOT NULL unique key가 먼저다(mysql.upsert.not_null_unique_first)); PostgreSQL 12+:ON CONFLICT (cols) DO UPDATE SET— 충돌 대상은 validator가 허용한 충돌 key 하나다(upsert에는 삽입 행을 만날 수 있는 key가 정확히 하나다. 다른 unique key에서 만나는 행은 unique 위반이다,postgres.upsert.other_key_conflicts); SQLite 3.46+: PostgreSQL과 같다(sqlite.upsert.other_key_conflicts). conformance vectorupsert_one_key는 세 database에서 같은 결과를 준다DIA-10
groupLimit(n)— MySQL 8.0.2+ / MariaDB 10.2+:ROW_NUMBER() OVER (…); PostgreSQL 12+: same; SQLite 3.46+: sameDIA-11
tuple<ColA>With<ColB>— MySQL 8.0.2+ / MariaDB 10.2+:(a, b) IN ((?, ?), …); PostgreSQL 12+: same; SQLite 3.46+:(a, b) IN (VALUES (?, ?), …)DIA-12
orderByRandom()— MySQL 8.0.2+ / MariaDB 10.2+:RAND(); PostgreSQL 12+:random(); SQLite 3.46+:random()DIA-13 nullable 컬럼의
orderBy<Col>Asc()/Desc()— MySQL 8.0.2+ / MariaDB 10.2+: 기본: 오름차순 NULL 먼저, 내림차순 NULL 나중 (mysql.order.null_first_asc); PostgreSQL 12+:ASC NULLS FIRST/DESC NULLS LAST: 이것 없이는 오름차순 NULL 나중 (postgres.order.null_last_asc); SQLite 3.46+: 기본: 오름차순 NULL 먼저, 내림차순 NULL 나중 (sqlite.order.null_first_asc)DIA-14 ORM 함수 — MySQL 8.0.2+ / MariaDB 10.2+: DSL 10절 참고
DIA-15
aes/hexstyles — MySQL 8.0.2+ / MariaDB 10.2+: 실행기에서 인증된 AES-256-GCM v2 처리 후hex가 있으면 hex text로 변환. 잘못된 hex text는 모든 database에서CODEC_DECODE다; PostgreSQL 12+: 실행기에서 인증된 AES-256-GCM v2 처리 후hex가 있으면 hex text로 변환. 잘못된 hex text는 모든 database에서CODEC_DECODE다; SQLite 3.46+: 실행기에서 인증된 AES-256-GCM v2 처리 후hex가 있으면 hex text로 변환. 잘못된 hex text는 모든 database에서CODEC_DECODE다DIA-16
ipstyle — MySQL 8.0.2+ / MariaDB 10.2+:INET6_ATON/INET6_NTOA; PostgreSQL 12+: client-side (16-byte packed,bytea); SQLite 3.46+: client-side (16-byte packed). SQL은 write 순서의 마지막 단계, 즉 모든 client-side 단계 뒤의 단계만 감싸므로, 한 단계는 그 column의 모든 뒤 단계도 SQL에서 실행될 때만 SQL에서 실행된다:ip hexcolumn은 MySQL에서도 주소를 client에서 pack하고 unpack하며(16-byte packed 뒤 hex text), 그 placeholder는 저장되는 hex text를 받는다(planner caseplanner_ip_hex_insert와planner_ip_hex_select)DIA-18 update 시각과 soft delete가 쓰는 clock — MySQL 8.0.2+ / MariaDB 10.2+:
p > 0인datetime(p)column은CURRENT_TIMESTAMP(p), 그 밖에는CURRENT_TIMESTAMP; PostgreSQL 12+: executor가 bind한, p로 자른 텍스트(nowbind slot); SQLite 3.46+: executor가 bind한, p로 자른 텍스트(nowbind slot)DIA-19 Executor 결과는 PostgreSQL parent IN list 확장 후
$n재번호화, client-side AES/inet codec, database별 DSN/driver 선택이다.DIA-20
github.com/polyspec/orm/packages/go:packages/go/orm은 MySQL만 연결한다. PostgreSQL 또는 SQLite를 열려면 해당 package를 side effect import한다.
import (
_ "github.com/polyspec/orm/packages/go/orm/pg" // driver "postgres"
_ "github.com/polyspec/orm/packages/go/orm/sqlite" // driver "sqlite"
)- DIA-21
github.com/polyspec/orm/packages/go: 필요한 driver package를 import하지 않으면orm.Connect는 필요한 import를 이름을 기준으로 적은CONFIG를 반환한다. Rust는 sqlx feature를 사용하고 PHP는 설치된 PDO extension을 사용한다.
세 database의 같은 결과를 유지하는 규칙
- DIA-22
UPDATE는 updated timestamp를 항상 명시한다(MySQL은mysql.go가 render하는 대로updated_ts = CURRENT_TIMESTAMP(p), PostgreSQL과 SQLite는nowbind slot으로 executor가 bind한, p로 자른 텍스트). soft delete도 같은 방식으로 column에 clock을 대입한다. MySQL의ON UPDATE는 다른 database에 대응하지 않는다. - DIA-23
plus와minus는 column을 table-qualified로 작성한다. - DIA-24 SQLite datetime은 UTC 기준
YYYY-MM-DD HH:MM:SStext이고datetime(p)컬럼은 정확히 p자리 소수를,time(p)컬럼의 time은HH:MM:SS와 정확히 p자리 소수를 가진다(renderer의 CHECK가 요구한다). 실행기는 bind slot이 싣는 p로 그 형식을 bind한다. 삽입이 생략한default now컬럼 값은 실행기가 UTC 시각으로 채운다. datetime이나 time 컬럼과 비교하거나 대입하는 문자열도 같은 형식으로 바꾼다.YYYY-MM-DD[ T]HH:MM:SS[.f]나HH:MM:SS[.f]는 0으로 p자리까지 채우고, p자리 뒤의 자리는 0이어야 하며 버려지고 0이 아니면CODEC_ENCODE를 반환한다.Z나±HH:MM을 포함한 문자열은 UTC로 변환한다. date 컬럼은YYYY-MM-DD를 받는다. 그 밖의 문자열은CODEC_ENCODE를 반환한다. 클라이언트는 MySQL과 PostgreSQL에서도 datetime과 time 값을 이 text 형식으로 bind한다.time(p)컬럼은 소수 정확히 p자리의 text로 읽으며, 그 text는 데이터베이스가 쓴다. MySQL은CAST(c AS CHAR), PostgreSQL은 p = 0이면to_char(c, 'HH24:MI:SS'), 그 밖에는substr(to_char(c, 'HH24:MI:SS.US'), 1, 9 + p)이다. PostgreSQL은time값을CAST(CAST(? AS text) AS time)로 bind한다. conformance vectordatetime_p0_roundtrip과time_p3_roundtrip이 그런 값을 삽입하고 같은 매개변수로 다시 읽어 비교하고, planner caseplanner_time_select와planner_time_insert가 statement를 고정한다. - DIA-25
decimal(P,S)컬럼은 MySQL에서DECIMAL(P,S), PostgreSQL에서NUMERIC(P,S), SQLite에서DECIMALINT(P,S)를 사용한다. SQLite는 정확한 값을 10^S배 한 부호 있는 정수로 저장하고, 실행기는 읽을 때 생성 모델의 고정 소수부 자릿수 십진 문자열로 변환한다. 선언은1 ≤ P ≤ 18,0 ≤ S ≤ P를 요구하므로 배율 정수가 부호 있는 64비트 정수 범위에 들어간다. 정수 decimal의 매니페스트에도scale: 0을 명시한다. 잘못된 텍스트, 초과 자릿수, 소수부 손실, 맞지 않는 저장 셀은CODEC_ENCODE또는CODEC_DECODE로 실패한다. - DIA-26
jsontext컬럼은 모든 방언에서 텍스트다. PostgreSQL은text, MySQL은LONGTEXT, SQLite는 TEXT다. 저장한 텍스트를 그대로 반환하므로 멤버 순서, 중복 키, 빈 객체와 빈 배열의 구분이 유지된다. ORM에는 JSON 경로 조건과 JSON 인덱스가 없으며, 데이터베이스 안에서 질의하는 데이터는 컬럼이나 자식 테이블로 만든다. - DIA-27
uuid컬럼은 PostgreSQL에서 네이티브uuid, MySQL에서char(36), SQLite에서 TEXT다. 클라이언트는 uuid 값을 텍스트로 bind한다. 모든 planner는 PostgreSQLuuid컬럼을CAST(col AS text)로 읽고 값을CAST(CAST(? AS text) AS uuid)로 bind한다(conformance vectoruuid_roundtrip). - DIA-28 MySQL은 TEXT, BLOB, JSON, geometry 컬럼의 리터럴 기본값을 식 형식
DEFAULT ('value')로 기록하며 MySQL 8.0.13 이상이 필요하다. import는 이를 리터럴로 다시 읽는다. - DIA-29 Boolean은 PostgreSQL
boolean, MySQLBOOLEAN/TINYINT(1)의 숫자0/1기본값, SQLite INTEGER 0/1을 사용한다. 정수가 아니라 boolean을 bind한다. - DIA-30 CHECK 표현식은 PostgreSQL과 SQLite에서 선언한 표현식을 그대로 생성한다. MySQL은 predicate를 반환하는
CASECHECK 정의를 거부하므로 명시적인 boolean 비교로 감싼다.NULL은UNKNOWN으로 유지되어 표준 CHECK 의미를 보존한다. MySQL CHECK 이름은 table이 아니라 database 범위이지만, renderer는 모든 CHECK 이름을 접두어 없이 그대로 쓴다. schema 검사는 제약·인덱스·생성 이름을 모두 전체 schema에서 유일하고 63바이트 이하로 유지하므로, 더 긴 생성 이름은 그것을 만드는 column이나 setting의name.length오류이며 줄이거나 hash를 붙이지 않는다(dbspec 이름). - DIA-31
bind_slots[].col_type은 placeholder가 받는 값의 dbspec type이다: 비교하거나 할당하는 column의 type(host style이 encode한 값도 저장되는 값이므로 column의 type이다), SQL 쪽 style 함수의 입력 type(INET6_ATON은text), 비교하는 column 함수의 결과 type, 상대 시각 함수의 간격은i32나f64, secret은text, config 값과 audit key는 그 column의 type,nowslot은datetime이다.parentslot은 비교하는 key column의 type인key_types를 가진다. 모든 slot에 type이 있고, type 없는 slot은IR_INVALID다.col_type은 SQL 쪽 style을 거친 뒤 placeholder가 받는 type이다. parameter의 논리 type은 그 앞의 type, 곧 비교하거나 할당하는 column이나 첫 stage 입력의 type이며ir.ParamUses가 이를 나열한다(protocol §5 "Parameters"). 클라이언트는 어떤 statement보다 먼저 각 parameter를 그것이 채우는paramslot의 논리 type과 slot의precision,scale,length로 검사하고, 그 밖의 값은CODEC_ENCODE다.host_styles가 있는 slot은 첫 stage의 encode가,transform이 있는 slot은 그 transform이 값을 검사한다. transform은 SQL NULL을 NULL로 bind하고 text가 아닌 다른 값은CODEC_ENCODE다. PostgreSQL에서 Rust client는 각 값을 그 slot의 dbspec type에 해당하는 PostgreSQL type으로 bind하고 server에 parameter나 column 정보를 묻지 않는다. 실행기는 bind 전에 SQLite 시간 값을 정규화한다. - DIA-32
bind_slots[].host_styles와columns[].styles는 실행기가 처리할 host stage를 기록한다. AES는docs/spec/codec.md에 정의한ORM-AES2\0authenticated ciphertext format, 12바이트 random nonce, AES-256-GCM, version key derivation을 사용한다. - DIA-33 AES column이 있는 entity는
aes_versionsetting으로 key version column을 선언한다. 읽기는 그 column을 한 번만 고른다. request가 고른 column이면 그것을, 아니면 숨은 column을 쓰며, 저장된 version으로 설정된 key version에서 key를 선택한다. key가 없으면CONFIG를, ciphertext가 유효하지 않으면CODEC_DECODE를 반환하며 이전 version에 current key를 사용하지 않는다. - DIA-35
contains는 모든 database에서 substring을 찾고, ASCII 글자는 대소문자 구분 없이, 다른 모든 문자는 code point로 비교한다: MySQL은 두 피연산자의 A–Z를 중첩REPLACE로 접고(위Lk), PostgreSQLCOLLATE "C"아래의ILIKE와 SQLiteLIKE는 ASCII 글자만 접는다. conformance vectorcontains_ascii_fold가 결과를 고정한다. - DIA-36 상대 시간 함수(
seconds_ago부터months_later까지)는 결과가 0001-01-01 00:00:00부터 9999-12-31 23:59:59.999999 밖이거나 interval이 그 범위의 폭(315537897600초, 5258964960분, 87649416시간, 3652059일, 119988개월)을 넘으면 NULL이다. 그래서 그 함수와의 비교와 그 비교의 부정이 모두 어떤 행과도 맞지 않는다. 범위를 넘으면 MySQL과 SQLite는 NULL을, 범위 아래에서는 0000년을 준다(mysql.relative.out_of_range_null,sqlite.relative.out_of_range_null). PostgreSQL은 표현할 수 없는 interval에서 실패하고 범위를 넘으면 10000년을 준다(postgres.relative.interval_out_of_range). 그래서 모든 dialect는 interval을CASE WHEN <interval> BETWEEN -<폭> AND <폭> THEN <interval> END로 감싸되 상수 접기가 표현할 수 없는 interval을 계산하지 않도록 함수 안에서 감싸고, 결과를CASE WHEN <결과> BETWEEN '0001-01-01 00:00:00' AND '9999-12-31 23:59:59.999999' THEN <결과> END로 감싼다. SQLite는 적용할 수 없는 interval에 이미 NULL을 주므로 결과 guard만 쓴다. conformance vectorrelative_time_out_of_range가 각 비교의 양쪽을 실행한다. 월 연산은 대상 월의 마지막 유효한 날을 유지한다(DSL-142). MySQLDATE_ADD와DATE_SUB, PostgreSQLmake_interval(months => n)은 스스로 그렇게 하고(mysql.relative.month_end_kept,postgres.relative.month_end_kept), SQLitedatetime은 대상 월의 끝을 넘는 날을 다음 달로 넘기므로 그 형태에floormodifier를 더한다(sqlite.relative.month_floor). - DIA-37 conformance vector의 결과는 세 database에서 같아야 한다.
get_query는 방언 SQL을 반환한다.
Schema definitions
이 절은 MySQL, PostgreSQL, SQLite가 스키마 객체를 정의하고 적용하고 catalog에 기록하는 방식을 정리하고, 중립 스키마 언어 dbspec이 각 기능을 지원하는지 결정한다. 기능은 (1) dbspec에 공통 정의가 하나 있고, (2) 그 정의가 MySQL, PostgreSQL, SQLite에서 같은 의미로 생성되며, (3) introspection이 숨은 표식 없이 catalog에서 같은 정의를 복원할 때만 지원한다. Renderer가 setting에서 생성하는 CHECK나 trigger 같은 동작은 catalog 텍스트가 선언한 renderer version의 출력과 같을 때만 복원한다.
모든 사실은 packages/go/internal/dialects의 공통 probe ID(make dialect-facts-check)를 인용한다. Probe는 자기 전용 일회용 database, schema, file에서 DDL, DML, catalog 질의를 실행하며 데이터베이스가 다르게 동작하면 실패한다. render.* probe는 제안한 rendering을 검사한다. Probe ID가 없는 사실은 공식 manual을 인용한다. 각 행은 **Decision:**으로 끝난다. T8.1 review는 이 기록이 결정하지 않는 정의다.
표기: "renderer CHECK"는 renderer가 타입이나 setting을 위해 생성하는 CHECK 제약이다. 정확한 텍스트와 이름은 T8.1에서 정하며, introspection은 catalog 텍스트가 해당 서버의 renderer 출력과 같을 때만 받아들인다.
Probe environment
| 항목 | 값 |
|---|---|
| MySQL | 8.4.11, 서버 sql_mode에 STRICT_TRANS_TABLES 포함, 모든 클라이언트가 config의 세션 sql_mode를 정함(mysql.session.sql_mode_set), lower_case_table_names=1, log_bin=1, log_bin_trust_function_creators=0, explicit_defaults_for_timestamp=1, 서버 시간대 SYSTEM(KST) (mysql.env.server) |
| PostgreSQL | 17.11, TimeZone=Asia/Seoul, database collation C, max_identifier_length=63 (postgres.env.server) |
| SQLite (probe) | modernc.org/sqlite v1.58.0의 3.53.4 (sqlite.env.library) |
| SQLite, Go client | modernc.org/sqlite v1.58.0(packages/go/go.mod), select sqlite_version() 결과 3.53.4 |
| SQLite, PHP client | PHP 8.4.26의 pdo_sqlite, select sqlite_version() 결과 3.53.4 |
| SQLite, TypeScript client | Node 26.11.1의 node:sqlite, select sqlite_version() 결과 3.53.4. package.json은 Node 26.12.0 이상을 허용하며 그 뒤 release의 bundled version은 측정하지 않았다 |
| SQLite 파일 이름 | DSN query가 남아 있는 파일 이름은 PHP PDO, node:sqlite, sqlite3 shell이 그대로 열어 bench.sqlite?_pragma=…를 만든다. modernc.org/sqlite는 query를 해석한다(TestSQLiteFileNameWithQuery의 sqlite.filename.* case). 클라이언트는 파일 이름을 정하기 전에 query를 제거한다 |
| SQLite, Rust client | sqlx 0.9 sqlite feature가 sqlite-bundled를 켠다. libsqlite3-sys 0.37.0(packages/rust/Cargo.lock)은 3.51.3을 포함하며, 같은 crate와 feature로 build한 프로그램의 sqlite3_libversion()으로 확인했다. Probe는 3.51.3에서 실행하지 않았다 |
Types
- DIA-38 정수 — MySQL 8.4:
TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT, strict mode에서 범위 검사(1264).tinyint(1)을 제외하면 display width를 제거한다; PostgreSQL 17:smallint/integer/bigint만 있으며 범위를 검사한다(22003).tinyint/mediumint는 없다(42704); SQLite: 선언한 이름은 affinity만 정한다. STRICT가 아닌 table은 어떤 값과 범위도 저장한다; Probe:mysql.int.tinyint_range,mysql.int.display_width_dropped,postgres.int.smallint_range,postgres.int.no_tinyint,sqlite.type.affinity_unenforced,sqlite.render.integer_check; 결정과 중립 rendering: Decision:i16,i32,i64로 지원한다. MySQLSMALLINT/INT/BIGINT, PostgreSQLsmallint/integer/bigint, SQLitesmallint/integer/bigint와 renderer CHECKtypeof(c) IN ('integer','null') AND c BETWEEN min AND max(i64는 범위 없음). 8비트·24비트 정수는 PostgreSQL에 해당 타입이 없으므로 지원하지 않는다; Introspection: MySQLCOLUMNS.COLUMN_TYPE, PostgreSQLpg_attribute.atttypid/format_type, SQLitepragma_table_info.type과sqlite_master.sql의 CHECK 텍스트 - DIA-39 Unsigned — MySQL 8.4:
UNSIGNED는 음수를 거부하고BIGINT UNSIGNED는 18446744073709551615까지 저장한다; PostgreSQL 17: 수식어가 없다(42601); SQLite: 이름의 일부로 받아들이고 무시한다; Probe:mysql.int.unsigned_rejects_negative,mysql.int.bigint_unsigned_exceeds_signed,postgres.int.no_unsigned,sqlite.type.affinity_unenforced; 결정과 중립 rendering: Decision: 지원하지 않는다. PostgreSQL과 SQLite에는 부호 없는 64비트 범위를 담는 타입이 없다. 음수 금지 규칙은 명시적인 CHECK 정의다; Introspection: MySQLCOLUMN_TYPE의unsigned접미사는 미지원으로 보고한다 - DIA-40 Boolean — MySQL 8.4:
BOOLEAN은tinyint(1)이며 2와 -1을 저장한다; PostgreSQL 17:boolean은 정수를 거부한다(42804); SQLite:TRUE/FALSE는 1/0이다.BOOLEAN은 NUMERIC affinity이며 어떤 값도 저장한다; Probe:mysql.bool.alias_tinyint,postgres.bool.rejects_integer,sqlite.bool.literals,mysql.render.bool_check,sqlite.render.bool_check; 결정과 중립 rendering: Decision:bool로 지원한다. MySQLtinyint(1)과 renderer CHECKb IN (0,1), PostgreSQLboolean, SQLiteBOOLEAN과 renderer CHECKb IN (0,1). 필드 이름은 boolean을 선택하지 않는다; Introspection: MySQLCOLUMN_TYPEtinyint(1)과 CHECK, PostgreSQLboolean, SQLite 타입과 CHECK 텍스트 - DIA-41 Decimal — MySQL 8.4:
DECIMAL(p,s), p ≤ 65(1426), s ≤ 30(1425). 초과 소수는 0에서 먼 쪽으로 반올림하고 overflow는 실패한다(1264); PostgreSQL 17:numeric(p,s), p ≤ 1000(22023), 음수 scale 허용. 반올림은 같고 overflow는 실패한다(22003); SQLite: Decimal 저장이 없다.DECIMAL(4,2)는 NUMERIC affinity이며 1.235를 real로 저장한다.DECIMALINT(p,s)는 INTEGER affinity다; Probe:mysql.decimal.scale_rounds,mysql.decimal.overflow_rejected,mysql.decimal.precision_limits,postgres.numeric.scale_rounds,postgres.numeric.overflow_rejected,postgres.numeric.precision_limits,sqlite.type.affinity_from_name,sqlite.render.decimal_check; 결정과 중립 rendering: Decision: 1 ≤ p ≤ 18, 0 ≤ s ≤ p인decimal(p,s)로 지원한다. MySQLDECIMAL(p,s), PostgreSQLnumeric(p,s), SQLite는 값 × 10^s를 저장하는DECIMALINT(p,s)와 renderer CHECKtypeof(d) IN ('integer','null') AND d BETWEEN -(10^p-1) AND 10^p-1. 클라이언트가 소수 s자리나 전체 p자리를 넘는 값을 어떤 statement보다 먼저CODEC_ENCODE로 거부하므로(tests/bind/vectors.json의decimal-scale과decimal-precision) 데이터베이스 반올림은 쓰지 않는다; Introspection: MySQLNUMERIC_PRECISION/NUMERIC_SCALE, PostgreSQLnumeric_precision/numeric_scale, SQLite 선언 타입 텍스트 - DIA-42 부동소수점 — MySQL 8.4:
FLOAT는 4바이트,DOUBLE은 8바이트. NaN과 무한대 값이 없다(1367); PostgreSQL 17:real4바이트,double precision8바이트. NaN과 Infinity를 저장한다; SQLite:REAL은 8바이트이며 무한대를 저장한다; Probe:mysql.float.single_precision,mysql.float.no_infinity,postgres.float.real_single_precision,postgres.float.nan_infinity,sqlite.float.infinity; 결정과 중립 rendering: Decision:f64를 지원한다. MySQLDOUBLE, PostgreSQLdouble precision, SQLiteREAL. 값은 유한하며 클라이언트가 NaN과 무한대를 거부한다. SQLite에 4바이트 float가 없으므로f32는 지원하지 않는다; Introspection: MySQLDATA_TYPE, PostgreSQLatttypid, SQLite 선언 타입 - DIA-43
varchar(n)— MySQL 8.4: 길이는 문자 수다(1406). 초과한 뒤쪽 공백은 제거한다. utf8mb4 n ≤ 16383(1074); PostgreSQL 17: 길이는 문자 수다(22001). 초과한 뒤쪽 공백은 제거한다. n ≤ 10485760(22023); SQLite: 길이를 검사하지 않는다; Probe:mysql.varchar.length_in_characters,mysql.varchar.trailing_spaces_truncated,mysql.varchar.max_length,postgres.varchar.length_in_characters,postgres.varchar.trailing_spaces_truncated,postgres.varchar.max_length,sqlite.varchar.length_unenforced,sqlite.render.varchar_check; 결정과 중립 rendering: Decision: 1 ≤ n ≤ 16383인varchar(n)으로 지원한다. MySQL과 PostgreSQLvarchar(n), SQLitevarchar(n)과 renderer CHECKlength(v) <= n. 클라이언트는 뒤쪽 공백을 포함해 n자를 넘는 값을 어떤 statement보다 먼저CODEC_ENCODE로 거부하므로(conformance vectorvarchar_length_rejected) 데이터베이스의 공백 제거는 쓰지 않는다; Introspection: MySQLCHARACTER_MAXIMUM_LENGTH, PostgreSQLatttypmod/character_maximum_length, SQLite 선언 타입과 CHECK 텍스트 - DIA-44
char(n)— MySQL 8.4: 읽을 때 뒤쪽 공백을 제거한다; PostgreSQL 17: 공백을 채운 값을 반환한다; SQLite: 채우지 않는다; Probe:mysql.char.trailing_spaces_removed,postgres.char.padded; 결정과 중립 rendering: Decision: 지원하지 않는다. MySQL과 PostgreSQL의 저장·반환 값이 다르다; Introspection: 미지원으로 보고한다 - DIA-45
text— MySQL 8.4:TEXT는 65535바이트 이하(1406),LONGTEXT는 4 GiB 이하. 리터럴 기본값은DEFAULT ('x')가 필요하고(1101) index는 prefix가 필요하다(1170); PostgreSQL 17: 길이 없는text. Probe에서 1 MiB를 저장했다. 필드 한도는 1 GB; SQLite:TEXT,SQLITE_MAX_LENGTH(기본 10^9바이트) 한도; Probe:mysql.text.max_bytes,mysql.text.literal_default_needs_expression,mysql.text.index_needs_prefix,postgres.text.unlimited; 결정과 중립 rendering: Decision:text로 지원한다. MySQLLONGTEXT, PostgreSQLtext, SQLiteTEXT.text컬럼은 key나 index에 포함할 수 없다. 최대 값은 PostgreSQL 1 GB 필드 한도, SQLiteSQLITE_MAX_LENGTH, MySQLmax_allowed_packet중 가장 작은 값이다. Ordered JSON은 codec setting을 가진text다; Introspection: MySQLDATA_TYPE, PostgreSQLatttypid, SQLite 선언 타입 - DIA-46 text의 U+0000 — MySQL 8.4: 저장한다; PostgreSQL 17: 거부한다(22021); SQLite: 저장하며
length()는 그 앞에서 멈춘다; Probe:mysql.text.nul_preserved,postgres.text.nul_rejected,sqlite.text.nul_preserved; 결정과 중립 rendering: Decision: U+0000은 중립 text 값이 아니다. 모든 데이터베이스에서 클라이언트가text와varchar의 U+0000을 어떤 statement보다 먼저CODEC_ENCODE로 거부한다(conformance vectortext_nul_rejected); Introspection: 해당 없음 - DIA-47 Bytes — MySQL 8.4:
BINARY(n)은 0바이트로 채우고VARBINARY(n)은 길이를 검사한다(1406).LONGBLOB; PostgreSQL 17:bytea는 길이가 없다(42601); SQLite:BLOB은 길이가 없다; Probe:mysql.binary.zero_padded,mysql.varbinary.length_enforced,postgres.bytea.no_length; 결정과 중립 rendering: Decision: 길이 없는bytes를 지원한다. MySQLLONGBLOB, PostgreSQLbytea, SQLiteBLOB. 채움과 길이는 MySQL에만 있으므로binary(n)과varbinary(n)은 지원하지 않는다; Introspection: MySQLDATA_TYPE, PostgreSQLatttypid, SQLite 선언 타입 - DIA-48 UUID — MySQL 8.4: 타입이 없다(1064); PostgreSQL 17:
uuid는 중괄호와 대문자를 받고 소문자를 반환한다; SQLite: 타입이 없다; Probe:mysql.uuid.no_type,postgres.uuid.native_normalized,mysql.render.uuid_check,sqlite.render.uuid_check; 결정과 중립 rendering: Decision: 소문자 표준 텍스트를 담는uuid로 지원한다. PostgreSQLuuid, MySQLchar(36) CHARACTER SET ascii COLLATE ascii_bin과 renderer CHECKREGEXP_LIKE(u, '^[0-9a-f]{8}-…$', 'c'), SQLiteTEXT와 문자 class 36개의 renderer CHECKu GLOB. 클라이언트는 소문자 표준 텍스트를 쓰고 다른 형식은 어떤 statement보다 먼저CODEC_ENCODE로 거부하므로(conformance vectoruuid_form_rejected) PostgreSQL의 다른 입력 형식 변환은 쓰지 않는다; Introspection: PostgreSQLuuid, MySQL과 SQLite 타입과 CHECK 텍스트 - DIA-49 Enum — MySQL 8.4:
ENUM은 목록 밖의 값을 거부하고(1265), 생략한 NOT NULL 컬럼에 첫 값을 저장하며, 목록 위치로 정렬한다; PostgreSQL 17:CREATE TYPE … AS ENUM은 별도 타입 객체(USER-DEFINED,pg_enum)이며 목록 밖의 값을 거부하고(22P02) 목록 위치로 정렬한다; SQLite:enum('a','b')는 문법 오류다; Probe:mysql.enum.rejects_unknown,mysql.enum.not_null_first_value_default,mysql.enum.sorts_by_position,postgres.enum.type_object,sqlite.enum.type_name_rejected; 결정과 중립 rendering: Decision: native enum은 지원하지 않는다. 값 목록은inCHECK 정의를 가진varchar(n)컬럼이며 모든 데이터베이스에서 텍스트로 정렬한다; Introspection: MySQLENUM컬럼과 PostgreSQL enum 타입은 미지원으로 보고한다 - DIA-50 JSON — MySQL 8.4:
JSON은 마지막 중복 키를 남기고 멤버 순서를 바꾼다; PostgreSQL 17:json은 텍스트를 유지하고jsonb는 마지막 중복 키를 남기며 순서를 바꾼다; SQLite: 텍스트다.json()은 중복 키를 유지한다; Probe:mysql.json.normalizes,postgres.json.text_jsonb_normalized,sqlite.json.text_kept; 결정과 중립 rendering: Decision: native JSON 타입은 지원하지 않는다. Ordered JSON은 ordered-json codec setting을 가진text컬럼이다; Introspection: MySQLJSON, PostgreSQLjson/jsonb컬럼은 미지원으로 보고한다
출처: MySQL numeric types, MySQL string types, MySQL storage requirements, MySQL ENUM, MySQL JSON, PostgreSQL numeric types, PostgreSQL character types, PostgreSQL binary data, PostgreSQL UUID, PostgreSQL enum, PostgreSQL JSON, SQLite datatypes, SQLite STRICT tables, SQLite limits.
- DIA-51 SQLite STRICT table은 storage class를 검사하지만 타입 이름은
INT,INTEGER,REAL,TEXT,BLOB,ANY만 허용한다(sqlite.strict.enforces_types,sqlite.strict.type_names_limited). 따라서 STRICT table은 catalog에varchar(n)이나DECIMALINT(p,s)를 유지할 수 없고 길이와 scale을 복원할 수 없다. Decision: SQLite table은 STRICT로 만들지 않는다. 선언 타입 이름이 중립 타입을 담고 renderer CHECK가 이를 검사한다.
Date and time
- DIA-52
date— MySQL 8.4:DATE. Strict mode는0000-00-00을 거부한다(1292); PostgreSQL 17:date, 기원전 4713년부터 5874897년까지; SQLite: 타입이 없으며 어떤 텍스트도 저장한다; Probe:mysql.date.zero_rejected,sqlite.datetime.text_unvalidated,sqlite.render.date_check; 결정과 중립 rendering: Decision: 0001-01-01부터 9999-12-31까지의date로 지원한다. MySQLDATE, PostgreSQLdate, SQLiteDATE와 renderer CHECKd IS date(d); Introspection: MySQLDATA_TYPE, PostgreSQLatttypid, SQLite 타입과 CHECK 텍스트 - DIA-53
time— MySQL 8.4:TIME은 -838:59:59부터 838:59:59까지의 interval이다(그 밖은 1292); PostgreSQL 17:time은 하루 중 시각이며 24:00:00을 받는다. 25:00:00은 실패한다(22008); SQLite 3.54.1(Go 드라이버가 고정해 쓰는 버전): 타입이 없고time('24:00:00')은 NULL을 반환하며 renderer CHECK가 이를 거부한다; Probe:mysql.time.interval_range,postgres.time.day_range,sqlite.render.time_check,mysql.render.time_of_day_check,postgres.render.time_of_day_check; 결정과 중립 rendering: Decision: 24:00:00 미만의 하루 중 시각인time(p)로 지원한다. MySQLTIME(p)와 renderer CHECKv >= '00:00:00' AND v < '24:00:00', PostgreSQLtime(p)와 renderer CHECKv < '24:00:00', SQLiteTIME텍스트와 p = 0의 renderer CHECKv IS time(v) AND v < '24:00:00'(소수 형식은 T8.1에서 정한다).postgres.render.time_of_day_check는 아직 실행하지 않았다; Introspection: 타입과 CHECK 텍스트 - DIA-54 소수 초 — MySQL 8.4: 0~6자리(그 이상은 1426). 초과 자릿수는 반올림한다.
(6)없는DATETIME(6) DEFAULT CURRENT_TIMESTAMP는 실패한다(1067); PostgreSQL 17: 0~6자리.timestamp(7)은 6이 된다. 초과 자릿수는 반올림한다; SQLite: 텍스트.strftime('%f')는 소수 세 자리이며CURRENT_TIMESTAMP는 소수가 없다; Probe:mysql.datetime.max_precision,mysql.datetime.fraction_rounds,mysql.datetime.default_precision_must_match,postgres.timestamp.precision_capped,postgres.timestamp.fraction_rounds,sqlite.datetime.now_is_utc; 결정과 중립 rendering: Decision: precision은 0~6이다. 클라이언트가 선언한 자리 뒤의 0이 아닌 숫자를 어떤 statement보다 먼저CODEC_ENCODE로 거부하고 그 자리의 0은 받아서 버리므로(conformance vectordatetime_precision_rejected) 데이터베이스 반올림은 쓰지 않는다; Introspection: MySQLDATETIME_PRECISION, PostgreSQLdatetime_precision, SQLite 타입 텍스트 - DIA-55
DATETIME/timestamp— MySQL 8.4:DATETIME은 쓴 wall clock 값을 유지하고 offset이 있는 리터럴을 session 시간대로 변환한다. 0001-01-01부터 9999-12-31까지 받는다.TIMESTAMP는 session 시간대와 UTC 사이를 변환하며 UTC 1970-01-01 00:00:01부터 2038-01-19 03:14:07까지만 받는다(1292); PostgreSQL 17:timestamp는 리터럴의 offset을 버린다.timestamptz는 시점을 저장하고 session 시간대로 표시한다. 0001-01-01, 9999-12-31,infinity를 받는다; SQLite: 타입이 없으며 텍스트다; Probe:mysql.datetime.no_conversion,mysql.datetime.offset_literal_converted,mysql.datetime.range,mysql.timestamp.range_2038,mysql.timestamp.session_time_zone,postgres.timestamp.ignores_offset,postgres.timestamptz.session_time_zone,postgres.timestamp.range,mysql.render.datetime_utc_session,postgres.render.timestamp_utc_session,sqlite.render.datetime_check; 결정과 중립 rendering: Decision: 0001-01-01부터 9999-12-31까지 시간대 없는 local date-time인datetime(p)로 지원한다. MySQLDATETIME(p), PostgreSQLtimestamp(p), SQLite는 소수 p자리의YYYY-MM-DD HH:MM:SS텍스트DATETIME과 renderer CHECK(길이,datetime(), 24 미만의 시, 숫자). 모든 연결은 UTC로 읽고 쓴다. 시간대 정보가 있는 시점(MySQLTIMESTAMP, PostgreSQLtimestamptz)은 지원하지 않는다; Introspection: MySQLDATA_TYPE, PostgreSQLatttypid(timestamp와timestamptz), SQLite 타입과 CHECK 텍스트 - DIA-56 현재 시각 — MySQL 8.4:
DATETIME의CURRENT_TIMESTAMP는 session wall clock이다(+09:00값은+00:00보다 9시간 뒤).NOW()는 문장 시작 시각이다; PostgreSQL 17:now()는 transaction의 모든 문장에서 transaction 시작 시각이다; SQLite:datetime('now')와CURRENT_TIMESTAMP는 UTC이며'now'는 한 문장 안에서 같다; Probe:mysql.current_timestamp.session_time_zone,mysql.now.precision_truncates,mysql.now.compare_six_digits,postgres.now.transaction_start,postgres.now.precision_rounds,postgres.now.compare_six_digits,sqlite.datetime.now_is_utc; 결정과 중립 rendering: Decision: statement의 clock 값은 하나이며, 쓸 때 자르고 비교할 때 여섯 자리를 유지한다(protocol "Clock"). MySQL은 자르는CURRENT_TIMESTAMP(p)를 쓰고(mysql.now.precision_truncates)NOW(6)와 비교한다(mysql.now.compare_six_digits). PostgreSQL은timestamp(p < 6)에 반올림하므로(postgres.now.precision_rounds) 클라이언트가 쓰는 모든 clock에 p로 자른nowslot을,nowvalue function에 여섯 자리 slot을 bind한다(postgres.now.compare_six_digits). SQLite도nowslot을 같은 방식으로 bind하고 clock과 비교하는datetime(p < 6)컬럼을 여섯 자리로 늘린다; Introspection: 기본값 텍스트(기본값 참조)
시간대 결정(owner: UTC 연결 규칙을 가진 local date-time):
- DIA-57 정의.
datetime(p)는 시간대 정보가 없는 wall clock date-time이다. 실행 규칙은 모든 연결이 UTC로 읽고 쓰는 것이다. 클라이언트는 각 연결에서 MySQLtime_zone='+00:00', PostgreSQLTimeZone='UTC'를 설정하고, offset이 있는 값은 bind 전에 UTC로 변환하며, offset 리터럴을 보내지 않는다. SQLite는 클라이언트가 쓴 UTC 텍스트 형식을 저장한다. 시간대 없는 date-time을 주는 driver(PyMySQL, psycopg)는 연결 시간대의 wall clock을 주므로, 클라이언트는 그 값을 연결 시간대로 읽으며 process의 지역 시간대로 읽지 않는다. 그래서 모든 읽기와 optimisticupdated_ts비교의 결과는 process의 시간대와 무관하다. 프로그램이 bind로 넘기는 시간대 없는 date-time, 곧 RustNaiveDateTime과 같은 시간대 없는 Pythondatetime도 연결 시간대(UTC)의 wall clock이다. PHP를 뺀 모든 클라이언트는 process 시간대를TZ에서 얻고, PHP는TZ를 읽지 않고date.timezone에서 얻는다. database client를 실행하는 모든 검사는 Makefile의TEST_TZ(Asia/Seoul)로 client를 실행한다.WITH_TEST_ENV가TZ를 export하고,TZ로date.timezone을 정하는tests/timezone/date.ini를 PHP ini scan 경로에 더하며, conformance checker는 PHP runner에-d date.timezone을 넘긴다. 그러면 값을 지역 시각으로 읽는 클라이언트는 UTC machine에서도 실패한다. - 이유. 모든 catalog에서 타입을 정확히 복원한다. 0001-01-01부터 9999-12-31까지의 범위를 세 데이터베이스가 받는다(
mysql.datetime.range,postgres.timestamp.range). UTC 규칙에서 시각 함수가 일치한다. MySQLNOW(6)는UTC_TIMESTAMP(6)와 같고(mysql.render.datetime_utc_session), PostgreSQLCURRENT_TIMESTAMP를timestamp로 cast한 값은now() AT TIME ZONE 'UTC'와 같으며(postgres.render.timestamp_utc_session), SQLite'now'는 UTC다(sqlite.datetime.now_is_utc). Offset 리터럴은 MySQL이 변환하고 PostgreSQL이 버리므로(mysql.datetime.offset_literal_converted,postgres.timestamp.ignores_offset) 클라이언트는 이를 보내지 않는다. - DIA-58 결과. 데이터베이스는 UTC 규칙을 검사하지 않는다. 다른 시간대의 session을 쓰는 ORM 밖의 writer는 오류 없이 다른 wall clock을 저장하며 raw SQL도 같은 규칙을 따라야 한다. 시간대 정보가 있는 시점은 지원하지 않으므로 MySQL
TIMESTAMP의 1970~2038 한도(mysql.timestamp.range_2038)는 적용되지 않는다. 채택하지 않은 선택지는 모든 데이터베이스에서 그 범위로 제한한 시점, 그리고 introspection이 복원할 수 없는 MySQLDATETIME과 PostgreSQLtimestamptzrendering이다. - DIA-59 기존 컬럼. dbspec introspection은 PostgreSQL
timestamptz컬럼을 미지원으로 보고하므로, schema를 introspect하기 전에 그 컬럼을 변환한다.ALTER TABLE … ALTER COLUMN c TYPE timestamp(p) USING c AT TIME ZONE 'UTC'는 각 시점의 UTC wall clock을 유지한다(postgres.render.timestamptz_conversion). MySQLTIMESTAMP컬럼은+00:00session에서MODIFY로DATETIME(p)로 변환한다(mysql.render.timestamp_conversion). 호환 mapping은 없다. 남아 있는timestamptz나TIMESTAMP컬럼은 introspection이 미지원으로 보고한다.
출처: MySQL date and time types, MySQL DATETIME and TIMESTAMP, MySQL TIME, MySQL time zone support, PostgreSQL date/time types, PostgreSQL current date/time, SQLite date and time functions.
Null, defaults, identity and generated columns
- DIA-60 NOT NULL — MySQL 8.4: 기본값 없는 NOT NULL 컬럼을 생략하면 실패한다(1364); PostgreSQL 17: 실패한다(23502); SQLite: 실패한다. rowid table의 INTEGER가 아닌 PRIMARY KEY 컬럼은 NOT NULL을 선언하지 않으면 NULL을 받는다; Probe:
mysql.not_null.missing_value_rejected,postgres.not_null.missing_value_rejected,sqlite.not_null.missing_value_rejected,sqlite.pk.nullable_non_integer; 결정과 중립 rendering: Decision: 지원한다. Primary key 컬럼은 모든 데이터베이스에서NOT NULL을 명시해 생성한다; Introspection: MySQLIS_NULLABLE, PostgreSQLattnotnull, SQLitepragma_table_info.notnull - DIA-61 리터럴 기본값 — MySQL 8.4:
COLUMN_DEFAULT는 따옴표 없는 리터럴이다.TEXT/BLOB기본값은DEFAULT ('x')가 필요하며DEFAULT_GENERATED와 함께_utf8mb4\'x\'로 기록한다; PostgreSQL 17:pg_get_expr는 cast를 추가한다:'x'::character varying,'y'::text; SQLite:dflt_value는 쓴 텍스트다:'x',0; Probe:mysql.default.catalog_text,mysql.text.literal_default_needs_expression,postgres.default.catalog_text,sqlite.default.catalog_text; 결정과 중립 rendering: Decision: 컬럼의 중립 타입 리터럴을 지원한다. Introspection은 dialect별 catalog 형식을 리터럴로 정규화한다. MySQLtext리터럴은 식 형식을 쓴다; Introspection: MySQLCOLUMN_DEFAULT와EXTRA, PostgreSQLpg_attrdef와pg_get_expr, SQLitepragma_table_info.dflt_value - DIA-62 식 기본값 — MySQL 8.4:
DEFAULT (expr), 예:(uuid()),DEFAULT_GENERATED로 기록한다; PostgreSQL 17: 모든 식을 정규화한다:(1 + 1),now(),CURRENT_TIMESTAMP; SQLite: 괄호가 있는DEFAULT (expr)만 받는다. 바깥 괄호를 뺀 쓴 텍스트를 유지한다; Probe:mysql.default.catalog_text,postgres.default.catalog_text,sqlite.default.catalog_text,sqlite.default.expression_needs_parentheses; 결정과 중립 rendering: Decision: 시간 결정에 따른 시각 기본값만 지원한다. UUID 생성기(MySQLuuid(), PostgreSQLgen_random_uuid(), SQLite 없음)를 포함한 다른 식 기본값은 지원하지 않는다; Introspection: 리터럴 기본값과 같다 - DIA-63 자동 key — MySQL 8.4: Key 컬럼의
AUTO_INCREMENT(아니면 1075). 명시한 더 큰 key는 counter를 올린다. Rollback한 insert는 간격을 남긴다; PostgreSQL 17:GENERATED BY DEFAULT AS IDENTITY는 명시한 key 뒤로 진행하지 않아 다음 생성 key가 충돌한다(23505).ALWAYS는 명시한 key를 거부한다(428C9).serial은 identity가 아닌 sequence 기본값이다. Rollback은 간격을 남긴다; SQLite:INTEGER PRIMARY KEY AUTOINCREMENT는INTEGER PRIMARY KEY에만 쓸 수 있다. 명시한 key는 counter를 올린다. Rollback은 간격을 남기지 않는다.AUTOINCREMENT가 없으면 삭제한 가장 큰 key를 다시 쓴다; Probe:mysql.auto_increment.explicit_value_advances,mysql.auto_increment.rollback_leaves_gap,mysql.auto_increment.requires_key,postgres.identity.by_default_not_advanced,postgres.identity.always_rejects_explicit,postgres.identity.rollback_leaves_gap,postgres.serial.sequence_default,sqlite.autoincrement.explicit_value_advances,sqlite.autoincrement.rollback_no_gap,sqlite.rowid.reused_without_autoincrement,sqlite.autoincrement.integer_only; 결정과 중립 rendering: Decision: 생략하면 생성하는 단일 컬럼 부호 있는i64primary key로 지원한다. MySQLBIGINT AUTO_INCREMENT, PostgreSQLbigint GENERATED BY DEFAULT AS IDENTITY, SQLiteINTEGER PRIMARY KEY AUTOINCREMENT. Key는 다시 쓰지 않는다. 클라이언트는 이 컬럼의 명시 값을 거부하며, PostgreSQL에서 명시 key를 쓰는 apply 단계는 identity sequence를 가장 큰 key로 맞춘다. 간격은 의미에 포함하지 않는다; Introspection: MySQLEXTRAauto_increment, PostgreSQLattidentity/identity_generation, SQLitesqlite_master.sql의AUTOINCREMENT와sqlite_sequence - DIA-64 생성 컬럼 — MySQL 8.4:
VIRTUAL과STORED. 식을 정규화해 기록한다(예:(`a` + 1)). 명시 값은 실패한다(3105); PostgreSQL 17: 17은STORED만 있다(VIRTUAL은 42601).pg_get_expr는(a + 1)로 정규화한다. 명시 값은 실패한다(428C9); SQLite:VIRTUAL과STORED.pragma_table_xinfo.hidden은 2 또는 3이며 식은sqlite_master.sql에만 있다; Probe:mysql.generated.catalog_expression,mysql.generated.explicit_value_rejected,postgres.generated.stored_only,postgres.generated.explicit_value_rejected,sqlite.generated.virtual_and_stored,sqlite.generated.explicit_value_rejected; 결정과 중립 rendering: Decision: 지원하지 않는다. PostgreSQL 17에는VIRTUAL이 없고,STORED식에는 T8.1이 CHECK에만 정의하는 중립 식 집합과 dialect별 catalog 정규화가 필요하다. 어느 client도 MySQL 생성 컬럼의 식 text를 읽지 않는다.GENERATION_EXPRESSION은 non-ASCII literal의 UTF-8 byte를 한 번 더 인코딩한 값(café)을 담고, 모든 client는 그 text를 쓰기 전에 열을 보고한다. 그래서 dbspec은 생성 컬럼을 만들지 않는다; Introspection: 미지원으로 보고한다
출처: MySQL data type defaults, MySQL AUTO_INCREMENT, MySQL generated columns, PostgreSQL CREATE TABLE, PostgreSQL generated columns, SQLite CREATE TABLE, SQLite AUTOINCREMENT, SQLite generated columns.
Keys and indexes
- DIA-65 Primary key — MySQL 8.4: 제약 이름을 버리고 key 이름은
PRIMARY다; PostgreSQL 17: 제약 이름을 유지한다; SQLite: 이름은sqlite_master.sql에만 있고 index는sqlite_autoindex_<table>_1이다; Probe:mysql.pk.name_is_primary,postgres.pk.name_kept,sqlite.pk.name_not_reported; 결정과 중립 rendering: Decision: 이름 없이 지원한다. PostgreSQL은 기본 이름<table>_pkey를 쓰고 SQLite는 제약 이름을 쓰지 않는다; Introspection: MySQLSTATISTICSindexPRIMARY, PostgreSQLpg_constraintcontype='p', SQLitepragma_table_info.pk - DIA-66 Unique key — MySQL 8.4: NULL 여러 개 허용; PostgreSQL 17:
NULLS NOT DISTINCT가 아니면 NULL 여러 개 허용(23505); SQLite: NULL 여러 개 허용; Probe:mysql.unique.nulls_distinct,postgres.unique.nulls_not_distinct_option,sqlite.unique.nulls_distinct; 결정과 중립 rendering: Decision: 서로 다른 NULL로 지원한다.NULLS NOT DISTINCT는 지원하지 않는다; Introspection: MySQLSTATISTICS.NON_UNIQUE=0, PostgreSQLpg_index.indisunique와pg_constraint, SQLitepragma_index_list.unique/origin - DIA-67 Key 순서 — MySQL 8.4:
DESC를 저장한다(COLLATIOND); PostgreSQL 17:DESC를 저장한다(pg_get_indexdef); SQLite:DESC를 저장한다(pragma_index_xinfo.desc); Probe:mysql.index.descending,postgres.index.descending_partial_expression,sqlite.index.descending; 결정과 중립 rendering: Decision: 컬럼별 오름차순·내림차순을 지원한다. NULLS FIRST/LAST 선택지는 없다; Introspection: 표에 적은 대로 - DIA-68 Prefix key — MySQL 8.4:
v(10)을SUB_PART로 기록한다; PostgreSQL 17: 해당 문법이 없다(42883); SQLite: 해당 문법이 없다; Probe:mysql.index.prefix,postgres.index.prefix_rejected; 결정과 중립 rendering: Decision: 지원하지 않는다; Introspection: MySQLSUB_PART는 미지원으로 보고한다 - DIA-69 Partial index — MySQL 8.4: 없다(1064); PostgreSQL 17:
WHERE조건(pg_index.indpred); SQLite:WHERE조건(pragma_index_list.partial, 텍스트는sqlite_master.sql); Probe:mysql.index.partial_rejected,postgres.index.descending_partial_expression,sqlite.index.partial_expression; 결정과 중립 rendering: Decision: 지원하지 않는다; Introspection: 미지원으로 보고한다 - DIA-70 식 index — MySQL 8.4: Functional key part:
COLUMN_NAMENULL, 정규화한EXPRESSION; PostgreSQL 17: 식 key(pg_index.indkey0); SQLite: 식 key(pragma_index_info.cid-2, 이름 NULL); Probe:mysql.index.functional,postgres.index.descending_partial_expression,sqlite.index.partial_expression; 결정과 중립 rendering: Decision: 생성 컬럼과 같은 이유로 지원하지 않는다; Introspection: 미지원으로 보고한다 - DIA-71 Full-text index — MySQL 8.4:
FULLTEXTindex; PostgreSQL 17: 해당 index가 없고to_tsvector식에 대한 GIN을 쓴다; SQLite: FTS5 virtual table만 있다; Probe: (manual); 결정과 중립 rendering: Decision: 지원하지 않는다. SQLite에는 table의 full-text index가 없고 PostgreSQL 형식은 식 index다; Introspection: 미지원으로 보고한다 - DIA-72 Key 길이 — MySQL 8.4: Index key는 3072바이트 이하다. utf8mb4
varchar(768)은 받고varchar(769)는 거부한다(1071); PostgreSQL 17: 2704바이트를 넘는 B-tree 항목은 insert에서 실패한다(54000). 4바이트 문자 640개는 들어간다; SQLite: 한도가 없다; Probe:mysql.index.max_key_bytes,postgres.index.btree_entry_limit,postgres.index.varchar_640_four_byte_fits; 결정과 중립 rendering: Decision: 한 index의varchar컬럼 선언 길이 합은 640자 이하다. utf8mb4로 2560바이트이며 두 한도 안에 있다.postgres.index.varchar_640_four_byte_fits는 아직 실행하지 않았다; Introspection: 해당 없음 - DIA-73 Index와 unique 이름 — MySQL 8.4: Index 이름은 table 범위이고 64자 이하다(1059).
PRIMARY는 예약어다(1280); PostgreSQL 17: Index와 unique 제약 이름은 schema 이름공간을 공유한다(42P07); SQLite: Index 이름은 database에서 고유하다; Probe:mysql.index.name_table_scope,mysql.index.name_max_length,mysql.index.primary_name_reserved,postgres.index.name_schema_scope,postgres.unique.name_schema_scope,sqlite.index.name_database_scope; 결정과 중립 rendering: Decision: 이름은 아래 식별자 규칙을 따르고 index와 unique key 전체에서 schema 안에 고유하다.primary는 유효한 이름이 아니다; Introspection: MySQLSTATISTICS.INDEX_NAME, PostgreSQL index의pg_class, SQLitepragma_index_list.name
출처: MySQL CREATE INDEX, MySQL InnoDB limits, PostgreSQL CREATE INDEX, PostgreSQL partial indexes, PostgreSQL unique indexes, SQLite CREATE INDEX, SQLite partial indexes, SQLite expression indexes, SQLite FTS5.
Foreign keys
- DIA-74 동작 — MySQL 8.4:
RESTRICT,NO ACTION(둘 다 즉시),CASCADE,SET NULL. Catalog는 생략한 동작을NO ACTION으로, 쓴 동작은 쓴 대로 기록한다; PostgreSQL 17: 모든 동작.NO ACTION은 문장 끝에,RESTRICT는 즉시 검사한다; SQLite: 모든 동작.NO ACTION은 문장 끝에,RESTRICT는 즉시 검사한다; Probe:mysql.fk.rule_catalog,mysql.fk.restrict_row_order,sqlite.fk.restrict_row_order,postgres.fk.restrict_statement_end; 결정과 중립 rendering: Decision: delete와 update에restrict,cascade,set_null을 지원하며 명시 keywordRESTRICT,CASCADE,SET NULL로 생성한다. MySQL은NO ACTION을 즉시 검사하므로NO ACTION은 지원하지 않는다. 하나의DELETE가 참조되는 행과 그 행을 참조하는 행을 함께 지우면, MySQL과 SQLite는 참조되는 행이 행 순서에서 먼저 나올 때 거부하고 PostgreSQL은 받아들인다; 이식 가능한 코드는 참조하는 행을 먼저 지운다.; Introspection: MySQLREFERENTIAL_CONSTRAINTS.UPDATE_RULE/DELETE_RULE, PostgreSQLpg_constraint.confupdtype/confdeltype, SQLitepragma_foreign_key_list.on_update/on_delete - DIA-75
SET DEFAULT— MySQL 8.4: 받아서 기록하지만 부모 삭제는 실패한다(1451); PostgreSQL 17: 지원한다; SQLite: 지원한다; Probe:mysql.fk.set_default_acts_as_restrict,postgres.fk.set_default; 결정과 중립 rendering: Decision: 지원하지 않는다; Introspection: 미지원으로 보고한다 - DIA-76 Deferrable — MySQL 8.4: 없다(1064); PostgreSQL 17:
DEFERRABLE INITIALLY DEFERRED는 COMMIT에서 검사한다(23503); SQLite: 같다; Probe:mysql.fk.deferrable_rejected,postgres.fk.deferrable,sqlite.fk.deferrable; 결정과 중립 rendering: Decision: 지원하지 않는다. 순환은 nullable 컬럼과 이후 update로 만든다; Introspection: 미지원으로 보고한다 - DIA-77
MATCH— MySQL 8.4:MATCH FULL은MATCH_OPTION FULL로 기록하지만SHOW CREATE TABLE에서 빠지며 검사하지 않는다; PostgreSQL 17:MATCH FULL은 일부가 NULL인 key를 거부한다(23503); SQLite: 받지만NONE으로 기록하고 검사하지 않는다; Probe:mysql.fk.match_full_not_enforced,postgres.fk.match_full,sqlite.fk.match_not_enforced; 결정과 중립 rendering: Decision: 기본MATCH SIMPLE만 지원한다; Introspection: MySQLMATCH_OPTION, PostgreSQLconfmatchtype, SQLitepragma_foreign_key_list.match - DIA-78 검사 스위치 — MySQL 8.4: 항상 켜져 있다; PostgreSQL 17: 항상 켜져 있다; SQLite: 각 연결에서
PRAGMA foreign_keys = ON전까지 꺼져 있으며 transaction 안에서는 무시한다; Probe:sqlite.fk.off_by_default,sqlite.fk.pragma_ignored_in_transaction; 결정과 중립 rendering: Decision: 모든 SQLite 연결이 첫 transaction 전에foreign_keys를 켠다는 규칙으로 지원한다(Go DSN pragma, PHPPRAGMA, Nodenode:sqlite와 sqlx 기본값이 켠다); Introspection: 연결별PRAGMA foreign_keys - DIA-79 자식 index — MySQL 8.4: 제약 이름을 따른 index를 자식 컬럼에 만든다; PostgreSQL 17: 없다; SQLite: 없다; Probe:
mysql.fk.child_index_created,postgres.fk.no_child_index; 결정과 중립 rendering: Decision: foreign key는 자식 컬럼으로 시작하는 index를 명시적으로 선언해야 한다. 그러면 MySQL은 그 index를 쓰고 새로 만들지 않는다; Introspection: Index catalog - DIA-80 컬럼 타입 — MySQL 8.4: 자식과 부모 타입이 같아야 한다(3780); PostgreSQL 17:
integer가bigint를 참조할 수 있다; SQLite: 검사하지 않는다; Probe:mysql.fk.type_mismatch_rejected,postgres.fk.type_mismatch_allowed; 결정과 중립 rendering: Decision: 자식 컬럼 타입은 참조 컬럼 타입과 같다; Introspection: 컬럼 catalog - DIA-81 이름 — MySQL 8.4: Database에서 고유하다(1826); PostgreSQL 17: Table 범위다; SQLite:
pragma_foreign_key_list에 없고sqlite_master.sql에 남는다; Probe:mysql.fk.name_schema_scope,sqlite.fk.names_not_reported; 결정과 중립 rendering: Decision: 이름을 가지며 schema 전체에서 고유하다. SQLite introspection은CREATE TABLE텍스트에서 이름을 읽는다; Introspection: MySQLREFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME, PostgreSQLpg_constraint.conname, SQLitesqlite_master.sql - DIA-82 Cascade 행의 trigger — MySQL 8.4: Cascade로 바뀐 행에서 trigger가 실행되지 않는다; PostgreSQL 17: 실행된다; SQLite: 실행된다; Probe:
mysql.fk.cascade_skips_triggers,postgres.fk.cascade_fires_triggers,sqlite.fk.cascade_fires_triggers; 결정과 중립 rendering: Trigger 참조; Introspection: 해당 없음
출처: MySQL FOREIGN KEY constraints, PostgreSQL foreign keys, SQLite foreign key support, SQLite PRAGMA foreign_keys.
CHECK constraints
- DIA-83 검사 — MySQL 8.4: 검사한다(3819). NULL은 통과한다.
NOT ENFORCED는 검사하지 않는 제약을 저장한다; PostgreSQL 17: 검사한다(23514). NULL은 통과한다.NOT VALID는 위반 행을 유지한다; SQLite: 검사하며 NULL은 통과한다; Probe:mysql.check.enforced_null_passes,mysql.check.not_enforced_option,postgres.check.enforced_null_passes,postgres.check.not_valid,sqlite.check.enforced_null_passes; 결정과 중립 rendering: Decision: 검사하고 검증한 제약을 지원한다.NOT ENFORCED와NOT VALID는 지원하지 않는다; Introspection: MySQLTABLE_CONSTRAINTS.ENFORCED, PostgreSQLconvalidated - DIA-84 식 텍스트 — MySQL 8.4:
CHECK_CLAUSE는 정규화한다:(`a` in (1,2)), 문자열 리터럴에_utf8mb4introducer와 두 번의 escape,NOT은 부정하는 비교로 바꿔 씀(De Morgan),BETWEEN은 유지, 음수 literal은-(n). bool column 하나는 거부한다(3812); PostgreSQL 17:pg_get_constraintdef는IN을 cast가 있는= ANY (ARRAY[...])로,BETWEEN을 비교 두 개로,NOT BETWEEN을 그 부정으로 바꾸고NOT은 유지한다; SQLite:sqlite_master.sql에 쓴 그대로만 남는다; Probe:mysql.check.catalog_text,mysql.check.alter_rewrites_introducers,mysql.check.alter_writes_time_precision,mysql.check.not_pushed_down,mysql.check.between_kept,mysql.check.string_escaped_twice,mysql.check.bool_column_rejected,postgres.check.normalized_text,postgres.check.between_rewritten,postgres.check.not_kept,sqlite.check.text_verbatim; 결정과 중립 rendering: Decision: dbspec의 predicate 형식에 지원한다. MySQL이나 PostgreSQL이 바꿔 쓰거나 거부하므로not,between, column 하나는 형식에서 뺀다. Renderer가 dialect별 텍스트를 쓰고, introspection은 catalog 텍스트가 생성한 식의 해당 dialect 정규형과 같을 때만 제약을 받아들인다. MySQL 8.4는 non-ASCII literal을 가진CHECK_CLAUSE를 UTF-8 bytes를 한 번 더 인코딩해 쓰므로, introspection은 그런 제약의 본문을 바르게 쓰는SHOW CREATE TABLE에서 읽고CHECK_CLAUSE와 같은 형식으로 escape한다; Introspection: MySQLCHECK_CONSTRAINTS.CHECK_CLAUSE, PostgreSQLpg_get_constraintdef, SQLitesqlite_master.sql - DIA-85 비결정 함수 — MySQL 8.4: DDL에서 거부한다(3814); PostgreSQL 17: 허용한다; SQLite: 평가할 때 거부한다; Probe:
mysql.check.nondeterministic_rejected,postgres.check.nondeterministic_allowed,sqlite.check.nondeterministic_rejected; 결정과 중립 rendering: Decision: 중립 식 집합에는 시각, 난수, session 함수가 없다; Introspection: 해당 없음 - DIA-86 FK 동작 컬럼 — MySQL 8.4: 참조 동작이 있는 foreign key 컬럼은 CHECK에 쓸 수 없다(3823); PostgreSQL 17: 허용한다; SQLite: 허용한다; Probe:
mysql.check.fk_action_column_rejected; 결정과 중립 rendering: Decision: dbspec은cascade나set_nullforeign key의 컬럼을 쓰는 CHECK를 거부한다; Introspection: 해당 없음 - DIA-87 Fault 순서 — MySQL 8.4: CHECK와 unique key를 함께 어긴 행은 CHECK fault로 거부한다(3819). Foreign key는 행마다 검사하므로 첫 행이 둘째 행을 참조하는
INSERT하나를 거부한다(1452); PostgreSQL 17: CHECK fault로 거부한다(23514). Foreign key는 statement 끝에 검사하므로 그INSERT를 받아들인다; SQLite: CHECK fault로 거부한다. Foreign key는 statement 끝에 검사하므로 그INSERT를 받아들인다; Probe:mysql.fault.check_before_unique,mysql.fault.fk_statement_end,postgres.fault.check_before_unique,postgres.fault.fk_statement_end,sqlite.fault.check_before_unique,sqlite.fault.fk_statement_end; 결정과 중립 rendering: Decision: fault가 여럿인 statement는 모든 database에서 한 행의 CHECK fault를 그 행의 key fault보다 먼저 보고한다. PostgreSQL과 SQLite는 foreign key를 statement 끝에, MySQL은 행마다 보고한다. SQL statement는 오류 하나를 보고하며, fault 여럿을 나열하는 executor는 이 규칙의 순서를 따른다; Introspection: 해당 없음 - DIA-88 이름 — MySQL 8.4: Database에서 고유하다(3822); PostgreSQL 17: Table 범위다; SQLite: 검사하지 않으며 한 table의 두 제약이 같은 이름을 쓸 수 있다; Probe:
mysql.check.name_schema_scope,postgres.check.name_table_scope,sqlite.constraint.duplicate_names_accepted; 결정과 중립 rendering: Decision: 이름을 가지며 schema 전체에서 고유하다; Introspection: 식 텍스트와 같다
출처: MySQL CHECK constraints, PostgreSQL check constraints, PostgreSQL catalog information functions, SQLite CREATE TABLE CHECK, SQLite deterministic functions.
Comments, collation and identifiers
- DIA-89 Comment — MySQL 8.4:
COMMENT절: 컬럼은 1024자 이하(1629), table은 2048자 이하(1628); PostgreSQL 17: 실질 한도가 없는COMMENT ON(5000자 저장); SQLite: Comment 객체가 없다. SQL 주석은CREATE TABLE텍스트 안에 남는다; Probe:mysql.comment.column_limit,mysql.comment.table_limit,postgres.comment.unlimited,sqlite.comment.no_object; 결정과 중립 rendering: Decision: 물리 객체로 지원하지 않는다. SQLite는 ORM 소유 table이나 DDL 텍스트 안에만 comment를 둘 수 있으며 둘 다 표식이다. 설명은 dbspec 문서에 남고 생성하지 않는다; Introspection: MySQL과 PostgreSQL comment는 미지원으로 보고한다 - DIA-90 Collation — MySQL 8.4: 기본 utf8mb4 collation
utf8mb4_0900_ai_ci는 unique key에서a,A,á를 같게 본다(1062).utf8mb4_bin은 PAD SPACE다(a=a).utf8mb4_0900_bin은 NO PAD이며 code point 순서다; PostgreSQL 17: 여기서는 database collationC: 대소문자·악센트·공백을 구분하고 code point 순서다. 컬럼은COLLATE "C"를 선언할 수 있다; SQLite: 기본BINARY: byte 순서.NOCASE는 ASCII만 접는다; Probe:mysql.collation.default_accent_case_insensitive,mysql.collation.utf8mb4_bin_pads,mysql.collation.utf8mb4_0900_bin_codepoint,postgres.collation.c_codepoint,sqlite.collation.binary_codepoint,sqlite.collation.nocase_ascii_only; 결정과 중립 rendering: Decision: binary code point 순서 collation 하나만 지원한다. MySQLCHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin, PostgreSQL은 database 기본값과 관계없이 모든 text 컬럼에COLLATE "C", SQLiteBINARY. 다른 collation은 지원하지 않는다.postgres.collation.c_codepoint의COLLATE "C"컬럼 catalog 확인은 아직 실행하지 않았다; Introspection: MySQLCOLUMNS.COLLATION_NAME, PostgreSQLpg_attribute.attcollation, SQLitesqlite_master.sql의COLLATE - DIA-91 식별자 길이 — MySQL 8.4: 64자 이하(1059); PostgreSQL 17: 더 긴 이름은 notice와 함께 63바이트로 잘리며 63바이트까지 같은 이름은 충돌한다(42P07); SQLite: 한도가 없다; Probe:
mysql.ident.max_length,mysql.index.name_max_length,postgres.ident.truncated_to_63,sqlite.ident.no_length_limit; 결정과 중립 rendering: Decision: 이름은[a-z][a-z0-9_]*이며 63바이트 이하다. 더 긴 이름은 줄이거나 바꾸지 않고 거부한다; Introspection: 저장한 이름 - DIA-92 식별자 대소문자 — MySQL 8.4: 컬럼 이름은 대소문자를 구분하지 않는다(1060). Table 이름은
lower_case_table_names를 따른다(make orm-test-servers가 선언한 두 platform의 test 서버는 1:Foo는foo로 저장되고Foo와foo가 충돌, 1050); PostgreSQL 17: 따옴표 없는 이름은 소문자로 접는다(42701). 따옴표 있는 이름은 대소문자를 구분한다; SQLite: 대소문자를 구분하지 않는다; Probe:mysql.ident.column_case_insensitive,mysql.ident.table_case_server_setting,postgres.ident.case_folding,sqlite.ident.case_insensitive; 결정과 중립 rendering: Decision: 소문자 이름만 쓰므로 모든 접기 규칙에서 같은 이름이 된다; Introspection: 저장한 이름 - DIA-93 이름 범위 — MySQL 8.4: Index: table. CHECK, FK, trigger: database; PostgreSQL 17: Index, unique 제약: schema. CHECK, FK, trigger: table; SQLite: Index, trigger: database. 제약: 검사하지 않음; Probe:
mysql.index.name_table_scope,mysql.check.name_schema_scope,mysql.fk.name_schema_scope,mysql.trigger.name_schema_scope,postgres.index.name_schema_scope,postgres.check.name_table_scope,postgres.trigger.name_table_scope,sqlite.index.name_database_scope,sqlite.trigger.name_database_scope; 결정과 중립 rendering: Decision: 모든 index, 제약, trigger 이름은 schema 전체에서 고유하다; Introspection: 해당 없음 - DIA-94 이름공간 — MySQL 8.4: Schema는 database다; PostgreSQL 17: Database 안의 schema이며 table 이름은 schema마다 고유하다; SQLite: 한정 이름은 연결의 attach한 database를 참조한다; Probe:
postgres.namespace.relname_per_schema,sqlite.namespace.attached_databases; 결정과 중립 rendering: Decision: schema 한정 이름은 지원하지 않는다. dbspec 문서 하나는 연결의 현재 database나 schema다; Introspection: MySQLDATABASE(), PostgreSQLcurrent_schema(), SQLitemain
출처: MySQL CREATE TABLE, MySQL collations, MySQL pad attribute, MySQL identifier length, MySQL identifier case, PostgreSQL COMMENT, PostgreSQL collation, PostgreSQL identifiers, SQLite collating sequences, SQLite ATTACH.
Update-time stamping
- DIA-95 컬럼 절 — MySQL 8.4:
ON UPDATE CURRENT_TIMESTAMP는 다른 컬럼이 바뀌면 실행되고, 값이 바뀌지 않으면 실행되지 않으며, 명시 값이 우선한다; PostgreSQL 17: 해당 절이 없다(42601); SQLite: 해당 절이 없다; Probe:mysql.on_update.fires_on_change,mysql.on_update.skipped_when_unchanged,mysql.on_update.explicit_value_kept,postgres.on_update.unsupported,sqlite.on_update.unsupported; 결정과 중립 rendering: Decision: 컬럼 절은 MySQL에만 있으므로 지원하지 않는다; Introspection: MySQLEXTRAon update …는 미지원으로 보고한다 - DIA-96 Trigger — MySQL 8.4:
BEFORE UPDATErow trigger는NEW에 대입할 수 있고 값이 바뀌지 않아도 실행된다; PostgreSQL 17: 함수를 쓰는BEFORE UPDATErow trigger는NEW에 대입할 수 있고 값이 바뀌지 않아도 실행된다; SQLite:NEW에 대입할 수 없다.AFTER UPDATEtrigger가 별도UPDATE를 실행하며 값이 바뀌지 않아도 실행된다; Probe:mysql.trigger.new_assignable,mysql.trigger.update_fires_when_unchanged,postgres.trigger.update_fires_when_unchanged,sqlite.trigger.new_read_only; 결정과 중립 rendering: Decision: 기록용 trigger를 생성하지 않는다. 아래 참조; Introspection: 해당 없음
수정 시각 기록 결정(owner: 실행기 기록):
- DIA-97 정의. 수정 시각 기록은 table 컬럼의 ORM mapping setting이다. 실행기는 계획하는 모든
UPDATE에서 UTC 문장 시각으로 컬럼을 명시적으로 대입한다. Trigger,ON UPDATE절, 기본값 변경 같은 물리 객체는 만들지 않는다. 이 setting은schemaHash가 아니라manifestHash에 속하며 introspection은 이를 읽거나 만들지 않는다. - 이유. 실행기는 세 데이터베이스에서 같은 값을 쓰고, 컬럼 정의는 introspection이 정확히 복원하는 일반
datetime(p)로 남는다. MySQLON UPDATE CURRENT_TIMESTAMP는 MySQL에만 있고 값을 바꾸지 않는 update를 건너뛰므로(mysql.on_update.skipped_when_unchanged) 실행기와 다르며 계속 지원하지 않는다. - DIA-98 결과. Raw SQL과 ORM 밖의 writer는 컬럼을 기록하지 않는다. Trigger 기록은 채택하지 않았다. MySQL
SUPER나log_bin_trust_function_creators=ON(mysql.trigger.binlog_privilege_1419), PostgreSQL 함수 객체, SQLite의 행마다 추가 쓰기(sqlite.trigger.new_read_only)가 필요하다.
Triggers and audit context
- DIA-99 수준과 event — MySQL 8.4: Row trigger만 있다(
FOR EACH STATEMENT는 1064).INSERT/UPDATE/DELETE; PostgreSQL 17: Row와 statement trigger,TRUNCATE도 있다; SQLite: Row trigger만 있다; Probe:mysql.trigger.no_statement_level,postgres.trigger.statement_fires_on_zero_rows,sqlite.trigger.no_statement_level; 결정과 중립 rendering: Decision: 생성하는 trigger는INSERT,UPDATE,DELETE의 row trigger다; Introspection: MySQLTRIGGERS.ACTION_TIMING/EVENT_MANIPULATION/ACTION_ORIENTATION, PostgreSQLpg_trigger.tgtype, SQLitesqlite_master.sql - DIA-100 0행 문장 — MySQL 8.4: 실행되지 않는다; PostgreSQL 17: Statement trigger는 일치하는 행이 없는
UPDATE에도 실행된다(P0001). Row trigger는 실행되지 않는다; SQLite: 실행되지 않는다; Probe:mysql.trigger.zero_rows_not_fired,postgres.trigger.statement_fires_on_zero_rows,postgres.trigger.row_skips_zero_rows,sqlite.trigger.zero_rows_not_fired; 결정과 중립 rendering: Decision: row trigger만 쓰므로 일치하는 행이 없는 문장은 실패하지 않는다; Introspection: 해당 없음 - DIA-101
TRUNCATE— MySQL 8.4: Trigger event가 없다(1064).TRUNCATE TABLE은DELETEtrigger를 건너뛴다; PostgreSQL 17:BEFORE TRUNCATEstatement trigger가 실행된다; SQLite:TRUNCATE가 없다.WHERE없는DELETE는 row trigger를 실행한다; Probe:mysql.trigger.no_truncate_event,mysql.trigger.truncate_skips_delete_trigger,postgres.trigger.truncate_event,sqlite.trigger.delete_all_fires_row_triggers; 결정과 중립 rendering: Decision: 생성하는 trigger는 어느 데이터베이스에서도TRUNCATE를 다루지 않는다.immutable과audit은 행UPDATE와DELETE만 보장하며TRUNCATE는 보장 밖의 권한 작업이다; Introspection: 해당 없음 - DIA-102 Cascade 행 — MySQL 8.4: Foreign key 동작으로 바뀐 행에서 trigger가 실행되지 않는다; PostgreSQL 17: 실행된다; SQLite: 실행된다; Probe:
mysql.fk.cascade_skips_triggers,postgres.fk.cascade_fires_triggers,sqlite.fk.cascade_fires_triggers; 결정과 중립 rendering: Decision:cascade나set_nullforeign key의 자식 table에는audit과immutablesetting을 거부한다. 따라서 모든 데이터베이스가 같은 행 변경을 관찰한다; Introspection: 해당 없음 - DIA-103 권한 — MySQL 8.4: Binary logging과
log_bin_trust_function_creators=0에서SUPER가 없는 login은 trigger를 만들 수 없다(1419); PostgreSQL 17: Table의TRIGGER권한과 함수의EXECUTE권한; SQLite: 권한이 없다; Probe:mysql.trigger.binlog_privilege_1419; 결정과 중립 rendering: Decision: apply는 첫 trigger 문장 전에 권한을 확인하고 필요한 설정과 함께 실패한다; Introspection: MySQL@@log_bin,@@log_bin_trust_function_creators - DIA-104 Trigger 본문 객체 — MySQL 8.4: 본문이 trigger 안에 있다; PostgreSQL 17: 본문은
pg_proc의 별도 함수이며DROP TABLE후에도 남는다; SQLite: 본문이 trigger 안에 있다.DROP TABLE은 trigger도 삭제한다; Probe:postgres.trigger.function_separate_object,sqlite.trigger.dropped_with_table; 결정과 중립 rendering: Decision: renderer는 생성하는 PostgreSQL trigger마다 함수 하나를 소유한다. Diff와 apply는 trigger와 함께 함수를 만들고 삭제한다; Introspection: PostgreSQLpg_proc - DIA-105
NEW대입 — MySQL 8.4:BEFOREtrigger에서 허용; PostgreSQL 17:BEFORErow trigger에서 허용; SQLite: 허용하지 않는다; Probe:mysql.trigger.new_assignable,postgres.trigger.update_fires_when_unchanged,sqlite.trigger.new_read_only; 결정과 중립 rendering: 수정 시각 기록을 위한 사실; Introspection: 해당 없음 - DIA-106 Catalog 텍스트 — MySQL 8.4:
ACTION_STATEMENT는 주석을 포함해 쓴 그대로의 본문이다; PostgreSQL 17:prosrc는 쓴 그대로의 함수 본문이다.pg_get_triggerdef는 schema로 한정한 table을 포함한 정규화 텍스트를 반환한다; SQLite:sqlite_master.sql은 쓴 그대로의CREATE TRIGGER텍스트다; Probe:mysql.trigger.body_catalog_verbatim,postgres.trigger.catalog_text,sqlite.trigger.catalog_verbatim; 결정과 중립 rendering: Decision: 생성한 trigger는 catalog 텍스트가 선언한 renderer version의 출력과 같을 때만 복원한다. MySQL은 timing, event, 본문, PostgreSQL은pg_get_triggerdef와prosrc, SQLite는 전체 텍스트다. 주석 표식은 쓰지 않는다; Introspection: 표에 적은 대로 - DIA-107 이름 — MySQL 8.4: Database에서 고유하다(1359); PostgreSQL 17: Table 범위다; SQLite: Database에서 고유하다; Probe:
mysql.trigger.name_schema_scope,postgres.trigger.name_table_scope,sqlite.trigger.name_database_scope; 결정과 중립 rendering: Decision: 생성 이름은 dbspec 이름의 규칙으로 63바이트 이하다(plain form, 또는 처음 54 bytes,$, SHA-256 16진수 8자리). 생성 이름의 길이 때문에 table을 거부하지 않으며, 다른 생성 이름이나 선언된 이름과 같은 생성 이름은 두 출처를 적는name.duplicate오류다. 선언된 이름이 63바이트를 넘으면 여전히 거부한다; Introspection: 표에 적은 대로
Audit context 선택지. Audit trigger는 클라이언트가 transaction 안에서 설정한 작업 단위의 audit을 읽는다.
- PostgreSQL
set_config('<setting>', id, true)는 transaction 범위다.COMMIT뒤 같은 session에서는 빈 문자열, 설정한 적 없는 session에서는 NULL을 읽는다(postgres.context.set_config_local). - MySQL user variable은 session 범위이며
COMMIT뒤에도 남는다(mysql.context.user_variable_session_scope). Pool 연결은 클라이언트가 지우지 않으면 값을 유지한다. - SQLite에는 session 변수가 없다. Main table의 trigger는
TEMPtable을 읽도록 만들 수 있지만 실행할 때 실패한다(sqlite.context.temp_table_not_visible). Main table의 행은COMMIT뒤 모든 연결에 보인다(sqlite.context.main_table_shared).
선택지는 (1) 클라이언트가 transaction 안에서 id를 설정하고 MySQL과 SQLite에서는 transaction 끝에서 지운다는 규칙을 가진 위 데이터베이스별 방식, (2) 실행기가 audit 대상 행마다 쓰는 audit 컬럼이며 table 정의가 바뀐다.
- DIA-108 판정: 두 전달 방식 모두 쓰지 않는다. 감사 대상 row가 audit 기록의 key를 담는 자기 audit column을 갖는다. transaction이 audit 기록을 삽입하고 executor가 모든
INSERT와UPDATE에서 그 column을 쓰므로, 모든 database에서 row trigger는 바꾸는 audit 기록을NEW에서, 이전 audit 기록을OLD에서 읽는다. 생성된 trigger는 row를 JSON으로 만들지 않고 column 대 column으로 이력 table에 복사한다. physicalDELETE의OLD는 지우는 audit 기록이 아니라 이전 audit 기록이므로, 감사 대상 table은 soft delete column으로만 지운다. session 변수, transaction setting,orm__contexttable은 쓰지 않는다. 정의는 dbspec에 있다.
출처: MySQL trigger syntax, MySQL stored program binary logging, MySQL FOREIGN KEY and triggers, MySQL user variables, PostgreSQL CREATE TRIGGER, PostgreSQL trigger functions, PostgreSQL set_config, SQLite CREATE TRIGGER, SQLite DELETE.
DDL transactions, views and sequences
- DIA-109 Transaction 안의 DDL — MySQL 8.4: DDL은 열린 transaction을 commit한다. 문장 하나는 원자적이다(실패한
DROP TABLE a, missing은 아무것도 삭제하지 않는다, 1051); PostgreSQL 17: DDL은 transaction에 포함된다.CREATE INDEX CONCURRENTLY는 transaction 안에서 실행할 수 없다(25001); SQLite: DDL은 transaction에 포함된다.ALTER TABLE은 컬럼을 바꿀 수 없고ADD COLUMN은 상수가 아닌 기본값을 거부한다; Probe:mysql.ddl.implicit_commit,mysql.ddl.atomic_statement,postgres.ddl.transactional,postgres.ddl.concurrent_index_outside_transaction,sqlite.ddl.transactional,sqlite.alter.limited; 결정과 중립 rendering: Decision: transaction DDL에 의존하는 기능은 없다. Apply는 문장마다 journal을 쓰고 SQLite 컬럼 변경은 table을 다시 만든다; Introspection: 해당 없음 - DIA-110 View — MySQL 8.4:
VIEW_DEFINITION은 database로 한정한 이름과 alias로 다시 쓴다; PostgreSQL 17:pg_get_viewdef는 정규화 텍스트를 반환한다; SQLite:sqlite_master.sql은 쓴 그대로 유지한다; Probe:mysql.view.definition_rewritten,postgres.view.definition_normalized,sqlite.view.text_verbatim; 결정과 중립 rendering: Decision: 지원하지 않는다. View 본문은 중립 형식이 없는 dialect 질의다; Introspection: 미지원으로 보고한다 - DIA-111 Sequence — MySQL 8.4: 없다(1064); PostgreSQL 17:
CREATE SEQUENCE; SQLite: 없다; Probe:mysql.sequence.unsupported,postgres.sequence.object,sqlite.sequence.unsupported; 결정과 중립 rendering: Decision: 지원하지 않는다. 자동 key가 유일한 생성 번호다; Introspection: 미지원으로 보고한다 - DIA-112 기타 객체 — MySQL 8.4: Partition, event, procedure, engine; PostgreSQL 17: 함수, 타입, extension, partition; SQLite: Virtual table,
WITHOUT ROWID; Probe: (manual); 결정과 중립 rendering: Decision: renderer가 trigger용으로 소유하는 함수를 제외하고 지원하지 않는다; Introspection: 미지원으로 보고한다
출처: MySQL implicit commit, MySQL atomic DDL, PostgreSQL CREATE INDEX CONCURRENTLY, SQLite ALTER TABLE, MySQL CREATE VIEW, PostgreSQL CREATE SEQUENCE.
Catalog sources
| 객체 | MySQL 8.4 | PostgreSQL 17 | SQLite |
|---|---|---|---|
| Table | information_schema.TABLES(TABLE_TYPE='BASE TABLE', TABLE_COLLATION, TABLE_COMMENT) | relnamespace로 pg_namespace와 join한 pg_class(relkind='r') | sqlite_master(type='table') |
| 컬럼 | information_schema.COLUMNS(COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, GENERATION_EXPRESSION, COLLATION_NAME) | pg_attribute, format_type, pg_get_expr를 통한 pg_attrdef, attidentity, attgenerated, attcollation | pragma_table_xinfo와 AUTOINCREMENT, COLLATE, 제약 이름을 위한 sqlite_master.sql |
| Primary·unique key, index | information_schema.STATISTICS(INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLLATION, SUB_PART, EXPRESSION), TABLE_CONSTRAINTS | pg_index(indkey, indoption, indpred), pg_get_indexdef, pg_constraint | pragma_index_list, pragma_index_xinfo, sqlite_master.sql |
| Foreign key | REFERENTIAL_CONSTRAINTS, KEY_COLUMN_USAGE | pg_constraint(contype='f', conkey, confkey, confupdtype, confdeltype, confmatchtype, condeferrable) | pragma_foreign_key_list, 이름을 위한 sqlite_master.sql |
| CHECK | CHECK_CONSTRAINTS.CHECK_CLAUSE, TABLE_CONSTRAINTS.ENFORCED | pg_constraint(contype='c'), pg_get_constraintdef, convalidated | sqlite_master.sql |
| Trigger | information_schema.TRIGGERS(ACTION_TIMING, EVENT_MANIPULATION, ACTION_ORIENTATION, ACTION_STATEMENT) | pg_trigger(tgtype, tgfoid), pg_get_triggerdef, pg_proc.prosrc | sqlite_master(type='trigger') |
| Comment | COLUMNS.COLUMN_COMMENT, TABLES.TABLE_COMMENT | pg_description(col_description, obj_description) | 없음 |
| Sequence와 counter | TABLES.AUTO_INCREMENT | pg_sequences, pg_get_serial_sequence | sqlite_sequence |
Rendered statements
- DIA-113 renderer는 dbspec 문서 집합을 한 dialect의 statement로 바꾼다. 잘못된 집합은 대신 그 diagnostic을 돌려준다(manifest와 hash).
tests/dbspec/ddl.json이 렌더링되는 모든 형식의 정확한 statement를 나열하며 규범 text다. 이 절은 그 뒤의 규칙을 정한다. 그 파일의 모든 vector는make dbspec-ddl-check가 MySQL 8.4, PostgreSQL 17, SQLite에 적용한다.
Statement와 순서
- DIA-114 문서는
use순서로 렌더링한다(쓰이는 문서가 쓰는 문서보다 먼저, 같으면 문서 이름 순). 한 문서의 table은 문서 순서다. 쓰이는 table은 자기 문서가 한 번만 렌더링한다. - DIA-115 모든 identifier는 따옴표로 감싼다: MySQL은 backtick, PostgreSQL과 SQLite는 큰따옴표. keyword는 대문자이고 statement마다 끝 semicolon 없는 한 줄이다.
- DIA-116 table마다
CREATE TABLE하나. SQLite는 이어서 unique key마다CREATE UNIQUE INDEX하나. 그다음 index마다 이름 순으로CREATE INDEX하나. - DIA-117 모든 table 뒤에 MySQL과 PostgreSQL은 foreign key마다
ALTER TABLE … ADD CONSTRAINT … FOREIGN KEY하나를 table 순서와 key 이름 순으로 쓴다. SQLite는 foreign key를CREATE TABLE안에 쓰며, 뒤에 오는 table도 참조할 수 있다. - DIA-118 마지막으로
immutable과audit의 trigger를 table 순서로 쓴다. PostgreSQL trigger는 자신이 실행하는CREATE FUNCTION뒤에 온다.
CREATE TABLE 안
DIA-119 부분의 순서: 선언 순서의 column, primary key, MySQL과 PostgreSQL은 이름 순의 이름 있는
UNIQUEconstraint로 쓴 unique key, SQLite는 이름 순의 foreign key, column 순서의 renderer CHECK, 이름 순의 선언된 check. MySQL statement의 끝에는ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin을 붙인다.DIA-120 column은 이름, type,
NOT NULL이나NULL, 그다음 default 순으로 쓴다.
| dbspec | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
i16, i32, i64 | SMALLINT, INT, BIGINT | smallint, integer, bigint | smallint, integer, bigint |
bool | tinyint(1) | boolean | BOOLEAN |
decimal(p,s) | DECIMAL(p,s) | numeric(p,s) | DECIMALINT(p,s) |
f64 | DOUBLE | double precision | REAL |
varchar(n) | varchar(n) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin | varchar(n) COLLATE "C" | varchar(n) |
text | LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin | text COLLATE "C" | TEXT |
bytes | LONGBLOB | bytea | BLOB |
uuid | char(36) CHARACTER SET ascii COLLATE ascii_bin | uuid | TEXT |
date | DATE | date | DATE |
time(p) | TIME(p) | time(p) | TIME |
datetime(p) | DATETIME(p) | timestamp(p) | DATETIME |
- DIA-121 Identity. MySQL은
BIGINT NOT NULL AUTO_INCREMENT와 table 수준PRIMARY KEY, PostgreSQL은bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL과 table 수준PRIMARY KEY, SQLite는 column에INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT를 쓰고 table 수준 primary key는 없다. - DIA-122 Default. literal default는 literal의 dialect 형식(아래)으로 쓴다.
default now는 MySQLCURRENT_TIMESTAMP(p), PostgreSQLstatement_timestamp(), SQLite는 소수 자릿수가 정확히 p인 UTC text다. p = 0이면strftime('%Y-%m-%d %H:%M:%S', 'now'), p ≤ 3이면substr(strftime('%Y-%m-%d %H:%M:%f', 'now'), 1, 20 + p), p > 3이면 0을 p − 3개 붙인(strftime('%Y-%m-%d %H:%M:%f', 'now') || '0…')이며 모두 괄호로 감싼다. 이 default는 raw SQL을 위한 것이다: ORM insert는 빼먹은default now컬럼에 PostgreSQL과 SQLite에서는nowslot을 bind하고 MySQL에서는CURRENT_TIMESTAMP(p)에 맡긴다(protocol "Clock"). - DIA-123 Literal. 정수,
f64, 문자열은 문서대로 쓴다. MySQL은 backslash를 escape로 읽으므로 MySQL 문자열은 모든 backslash를 두 번 쓴다.true와false는 MySQL과 SQLite에서1과0, PostgreSQL에서TRUE와FALSE다. SQLite decimal literal은 정수 값 × 10^s다.decimal(13,2)의7.50은750이다. - DIA-124 Key.
PRIMARY KEY (…)는 key column을 나열한다. PostgreSQL은 기본 이름<table>_pkey를 유지한다. unique key는 MySQL과 PostgreSQL에서CONSTRAINT <name> UNIQUE (…), SQLite에서CREATE UNIQUE INDEX <name> ON <table> (…)이며, 그래서 SQLite catalog가 그 이름을 보고한다. 내림차순 index column 뒤에는DESC를 쓴다. - DIA-125 Foreign key.
RESTRICT,CASCADE,SET NULL로CONSTRAINT <name> FOREIGN KEY (…) REFERENCES <table> (…) ON DELETE <action> ON UPDATE <action>이다. - DIA-126 Check. 선언된 check는
CONSTRAINT <name> CHECK (<predicate>)다. column 이름은 따옴표로 감싸고,and,or,not,in,between,is null,is not null은 대문자, literal은 dialect 형식, 괄호는 적힌 대로 쓴다.
Renderer CHECK
- DIA-127 renderer CHECK는 dialect가 강제하지 않는 type을 강제한다. 이름은 dbspec 이름의 규칙으로
<table>$<column>이다: 63 bytes 이하이면 plain form이고, 아니면 처음 54 bytes,$, SHA-256의 16진수 8자리다. dbspec 이름에는$가 없으므로 선언된 이름과 겹치지 않는다. column마다 CHECK 하나가 column type의 모든 조건을 담는다.
| dbspec | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
i16, i32 | typeof(c) IN ('integer', 'null') AND c BETWEEN <min> AND <max> | ||
i64(identity 아님) | typeof(c) IN ('integer', 'null') | ||
bool | c IN (0, 1) | c IN (0, 1) | |
decimal(p,s) | typeof(c) IN ('integer', 'null') AND c BETWEEN -(10^p − 1) AND 10^p − 1 | ||
f64 | typeof(c) IN ('real', 'null') | ||
varchar(n) | length(c) <= n | ||
uuid | REGEXP_LIKE(c, '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', 'c') | c GLOB 문자 class 36개 | |
date | c IS date(c) | ||
time(0) | c >= '00:00:00' AND c < '24:00:00' | c < '24:00:00' | c IS time(c) AND c < '24:00:00' |
time(p), p > 0 | time(0)과 같음 | time(0)과 같음 | length(c) = 9 + p AND substr(c, 1, 8) IS time(substr(c, 1, 8)) AND substr(c, 1, 8) < '24:00:00' AND substr(c, 9, 1) = '.' AND substr(c, 10) GLOB 숫자 class p개 |
datetime(0) | length(c) = 19 AND c IS datetime(c) AND substr(c, 12, 2) < '24' | ||
datetime(p), p > 0 | length(c) = 20 + p AND substr(c, 1, 19) IS datetime(substr(c, 1, 19)) AND substr(c, 12, 2) < '24' AND substr(c, 20, 1) = '.' AND substr(c, 21) GLOB 숫자 class p개 |
- DIA-128 SQLite
f64CHECK는 위 schema definitions에 probe가 없다. REAL column은 정수와 숫자 text를 real 값으로 바꾸고 다른 text는 그대로 두는데, 이 CHECK가 그것을 거부한다(tests/dbspec/ddl.json의types-and-defaultscase). PostgreSQL은timestamp입력의 24시를 다음 날 자정으로 읽으므로'2026-01-01 24:00:00'은 유효한 date-time인2026-01-02 00:00:00으로 저장된다. MySQL은 그 입력을 거부하고(1292) SQLite는 renderer CHECK가 거부한다. 저장되는 값의 집합이 같으므로 PostgreSQL CHECK는 없다(같은 case). client는 24시를 보내지 않는다.
Trigger
DIA-129 생성 trigger 이름은 renderer CHECK 이름과 같은 규칙(dbspec 이름)으로 줄인
<table>$<event>이다:immutable_update,immutable_delete,audit_insert,audit_update,audit_delete. PostgreSQL trigger는 renderer가 소유한 같은 이름의 function을 실행한다. 거부는table <table> is immutable이나table <table> deletes through its soft delete columnmessage를 일으킨다: MySQLSIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '…', PostgreSQLRAISE EXCEPTION '…', SQLiteSELECT RAISE(ABORT, '…').DIA-130
immutable: 거부하는BEFORE UPDATE와BEFORE DELETErow trigger.DIA-131
audit:AFTER INSERTrow trigger는 이력 table에 action'insert', NULL 이전 audit 기록, 기록하는 모든 column의NEW값을 넣는다.AFTER UPDATErow trigger는'update',OLD.<audit column>, 기록하는 모든 column의NEW값을 넣는다.BEFORE DELETErow trigger는 거부한다. insert는 action, previous, 기록하는 column 순으로, 기록하는 column은 table의 column 순서로 적고 이력 key는 identity에 맡긴다(audit).tables가 있으면 두 trigger는 각각 이어서(NEW.<audit column>, '<table>')을 tables table의 record column과 name column에 넣고, 있는 row는 그대로 둔다: MySQL은INSERT INTO <tables table> (<record column>, <name column>) VALUES (NEW.<audit column>, '<table>') ON DUPLICATE KEY UPDATE <name column> = <name column>, PostgreSQL과 SQLite는ON DUPLICATE KEY UPDATE절 대신ON CONFLICT DO NOTHING을 쓴 같은 insert다.DIA-132 MySQL trigger 본문은 statement 하나이고,
tables가 있으면BEGIN <history insert>; <tables insert>; END다. PostgreSQL과 SQLite 본문도 statement를 같은 순서로 적고 각각 뒤에;를 붙인다. PostgreSQL function 본문은LANGUAGE plpgsql의BEGIN … RETURN NULL; END다(BEFORE거부는 반환하지 않는다). SQLite 본문은BEGIN …; END다.
Introspection
- DIA-133 introspection은 connection의 현재 database(MySQL), 현재 schema(PostgreSQL),
maindatabase(SQLite)를 읽어 호출자가 이름을 준 dbspec 문서 하나와 읽지 못한 객체 목록을 돌려준다.make dbspec-introspect-check는tests/dbspec/ddl.json의 모든 vector와 모든 schema 문서를 렌더링해 적용하고 database를 introspect한 뒤, 결과의 schema text가 table을 이름 순으로 둔 원본의 schema text와 같고 미지원 객체가 없기를 요구한다. 또tests/dbspec/introspect.json을 실행한다. 각 case는 그 dialect의 빈 database에 문서를 렌더링하고 statement를 실행한 뒤(statement의{schema}는 그 database 또는 schema의 이름 문자열로 치환하며, PostgreSQL case는 함께 지우는 두 번째 schema{schema}_b를 만들 수 있다), introspect한 문서의 canonical form과 미지원 객체[kind, table, name]를 기대한다. 네 번째 원소가 있으면 그 객체의 이유가 그 rule을 포함하는지 비교하고, 없으면 이유는 비교하지 않는다.make dbspec-introspect-<client>-check는make dbspec-introspect-check가 Go에서 하듯 같은 round trip과 case를 PHP 클라이언트, TypeScript 클라이언트, PHP 확장, Rust 클라이언트, Python 클라이언트로 실행한다(make dbspec-introspect-python-check는 venv의 python으로packages/python/tests/dbspec_introspect.py를 실행하고 MySQL, PostgreSQL, SQLite case를 검사한다).make dbspec-introspect-compare-check는 stress 모양의 20 table 문서(make bench의make dbspec-introspect-compare-bench는 2000 table stress 문서와 release Rust runner)를 각 dialect에 적용하고 runner 다섯 개(Go, PHP, TypeScript, Rust, Python, PHP 확장; PHP 확장은tests/dbspec/introspect의 runner이고 Python runner는packages/python/tests/dbspec_introspect_runner.py)를 실행해, 출력(canonical 문서, 그다음 미지원 객체마다! kind<TAB>table<TAB>name줄)이 같고, 원본 schema text를 가지며, 미지원 객체가 없기를 요구한다. 각 introspection의 시간을 출력하고 그것이 기준값 5초를 넘으면 경고하며, 시간은 검사를 실패시키지 않는다.
문서
- DIA-134 table은 이름 순, column은 catalog 위치 순, constraint와 index는 이름 순이다. 문서에는 diagram과 comment가 없다.
- DIA-135 이름이
dbspec$로 시작하는 table과 column은 모두 빼고 보고하지 않는다: plan history tabledbspec$plans, plan이 숨긴 table과 column, SQLite 작업 table이다(plans). - DIA-136 database에 렌더링되는 setting만 읽는다: trigger로 알아보는
immutable과audit. manifest setting은 문서에만 있다. 동기화하는 도구는 비교하는 문서에서 이를 유지한다.
Type과 column
| Catalog | MySQL COLUMN_TYPE | PostgreSQL format_type | SQLite 선언 type | dbspec |
|---|---|---|---|---|
| 16, 32, 64-bit 정수 | smallint, int, bigint | smallint, integer, bigint | renderer CHECK가 있는 smallint, integer, bigint | i16, i32, i64 |
| boolean | renderer CHECK가 있는 tinyint(1) | boolean | renderer CHECK가 있는 BOOLEAN | bool |
| decimal | decimal(p,s) | numeric(p,s) | renderer CHECK가 있는 DECIMALINT(p,s) | decimal(p,s) |
| 실수 | double | double precision | renderer CHECK가 있는 REAL | f64 |
| 길이 제한 text | utf8mb4_0900_bin의 varchar(n) | collation C의 character varying(n) | renderer CHECK가 있는 varchar(n) | varchar(n) |
| text | utf8mb4_0900_bin의 longtext | collation C의 text | CHECK 없는 TEXT | text |
| bytes | longblob | bytea | BLOB | bytes |
| uuid | renderer CHECK가 있는 ascii_bin의 char(36) | uuid | renderer CHECK가 있는 TEXT | uuid |
| date | date | date | renderer CHECK가 있는 DATE | date |
| 하루 중 시각 | renderer CHECK가 있는 time이나 time(p) | renderer CHECK가 있는 time(p) without time zone | p 하나의 renderer CHECK가 있는 TIME | time(p) |
| date-time | datetime이나 datetime(p) | timestamp(p) without time zone | p 하나의 renderer CHECK가 있는 DATETIME | datetime(p) |
DIA-137 MySQL은 precision 0을 괄호 없이 쓴다(
time,datetime). type, collation, character set이 이 표에 없는 column, renderer CHECK 없는 MySQLtinyint(1),char(36),timecolumn, CHECK가 어떤 dbspec type의 renderer 출력도 아닌 SQLite column은 미지원으로 보고한다.DIA-138 Null.
IS_NULLABLE,attnotnull,pragma_table_xinfo.notnull이null을 준다.DIA-139 Identity. MySQL
EXTRAauto_increment, PostgreSQLattidentityd, SQLiteINTEGER PRIMARY KEY AUTOINCREMENT인 단일 columnbigintprimary key는identity다. PostgreSQLattidentitya(ALWAYS)는 미지원이다.DIA-140 Default. literal default는 column의 canonical default 형식으로 읽는다. MySQL
COLUMN_DEFAULT는 escape를 푼 값(문자열, 숫자,bool이면1이나0)이다. PostgreSQLpg_get_expr는'-5'::integer,'a''b'::character varying,'2026-01-01'::date처럼 cast를 가진 literal을 주며 cast를 뗀다. SQLitedflt_value는 렌더링한 literal이며 decimal은 scale을 곱한 정수다. 시각 default는 renderer 출력과 같을 때default now다:DEFAULT_GENERATED인 MySQLCURRENT_TIMESTAMP나CURRENT_TIMESTAMP(p), PostgreSQLstatement_timestamp(), 그 precision의 SQLitestrftime식. 그 밖의 식은 미지원이다.
Key, index, foreign key
- DIA-141 primary key는 column 순서를 유지한다. MySQL
UNIQUEindex, PostgreSQLUNIQUEconstraint, origin이c인 SQLite unique index는 unique key이고, 그 밖의 index는 index이며DESC는COLLATION,indoption,pragma_index_xinfo.desc에서 읽는다. - DIA-142 prefix, partial, expression, full-text index, constraint 없는 PostgreSQL unique index, primary key가 아닌 SQLite
sqlite_autoindex, 이름에$가 있는 index는 미지원이다. - DIA-143 foreign key는 이름, column, 참조 table과 column, 두 action을 읽는다.
NO ACTION,SET DEFAULT, deferrable key,MATCH FULL, 참조 table이 다른 schema에 있는 PostgreSQL key는 미지원이다. SQLite foreign key 이름은sqlite_master의CREATE TABLEtext에서 온다.
Check
DIA-144
<table>$<column>이름의 check는 renderer CHECK다. column type에 대한 renderer 출력의 catalog 형식과 같아야 한다:bool은 MySQL(`c` in (0,1)),uuid는regexp_like(`c`,_utf8mb4\'^…$\',_utf8mb4\'c\'),time(p)는((`c` >= _utf8mb4\'00:00:00\') and (`c` < _utf8mb4\'24:00:00\')), PostgreSQL은 column 이름을quote_ident가 쓰는 대로 쓴CHECK ((c < '24:00:00'::time without time zone)), SQLite는 렌더링한 text 그대로다. MySQL은 table의ALTER TABLE마다CHECK_CLAUSE를 다시 써서 uuid pattern에_asciiintroducer를 주고 time literal의 introducer를 뺀다(mysql.check.alter_rewrites_introducers). 또time(p)column과 만나는 time literal을 0으로 된p자리 소수와 함께 쓴다(mysql.check.alter_writes_time_precision). 그래서 MySQL clause는 character set introducer와 time literal의 0인 소수를 뺀 뒤 비교한다. 이 template의 literal은 ASCII이고 0인 소수는 값을 바꾸지 않으므로 비교는 의미를 지킨다. 다른 renderer CHECK와 renderer CHECK가 아닌$이름은 미지원이다.DIA-145 그 밖의 check는 dbspec의 predicate로 읽어 canonical form으로 쓴다.
DIA-146 MySQL
CHECK_CLAUSE: 모두 괄호로 감싸고, keyword는 소문자, column은 backquote,in (a,b)는 공백 없음, 음수는-(n), 문자열은_utf8mb4와 MySQL 문자열 literal이며 그 text는 한 번 더 escape된다(clause 안의\'와\\).DIA-147 PostgreSQL
pg_get_constraintdef: 모두 괄호로 감싼 식을CHECK (…)가 감싸고, column은quote_ident가 필요할 때만 따옴표를 쓰며, literal과 column에 cast가 붙고('x'::text,(s)::text,'-1.50'::numeric,(1.5)::double precision),in은= ANY (ARRAY[…]),not in은<> ALL (ARRAY[…])이고, time과 date-time literal은 끝의 0 소수를 뺀다.DIA-148 SQLite:
CREATE TABLEstatement의CHECK (…)안 text이며 렌더링한 text다. decimal literal은 scale을 곱한 정수다.DIA-149 literal은 만나는 column의 값으로 읽어 canonical default 형식으로 쓴다. 그래서
datetime(6)column의'2100-01-01 00:00:00'은'2100-01-01 00:00:00.000000',boolcolumn의1은true가 된다. predicate로 읽히지 않거나 dbspec 규칙을 어기는 check는 미지원이다.
Trigger
- DIA-150 각 setting은 제 trigger의 이름을 보고 알아보며, 그 이름은 dbspec 이름의 규칙으로 재계산한다. trigger 개수로 알아보지 않는다.
<table>$immutable_update와<table>$immutable_delete이름의 trigger는immutablesetting을,<table>$audit_insert,<table>$audit_update,<table>$audit_delete이름의 trigger는auditsetting을 준다. 각각 그 setting의 renderer 출력과 같을 때다: MySQL timing, event,ACTION_STATEMENT, PostgreSQL은 table의 schema prefix를 뗀pg_get_triggerdef와 그 function의prosrc, SQLite는CREATE TRIGGERtext.auditparameter는 비교 전에audit_insert의 insert statement와audit_update의OLDcolumn에서 읽는다. insert는 action, previous, 기록하는 column을 적고, 기록하는 column은 audit column을 포함하며, insert가 적지 않은 table의 column은 schema text가 쓰는 대로 column 순서의exclude목록이 된다. audit 기록 table은 audit column의 유일한 foreign key의 대상 table이다.audit_insert의 둘째 insert는tables절, 그 대상과 두 column을 준다. 두 setting을 함께 쓴 table은 각 setting renderer 출력의 합집합을 가지며, trigger 집합은 그 합과 이름과 내용이 정확히 같아야 한다. 더 있거나, 빠지거나, 내용이 다른 trigger 하나는 그 table의 모든 trigger를 미지원으로 만들며, 그 밖의 모든 trigger도 마찬가지다.
미지원 객체
DIA-151 미지원 객체마다 kind, 있으면 table과 이름, 이유를 보고하며 문서에서는 뺀다. 목록은 table, kind, 이름 순이고 table이 없는 객체가 먼저 온다. kind는
column,index,unique,foreign_key,check,trigger(parse되지 않는immutable이나auditsetting마다 하나),view,routine,sequence,event,partition(MySQL이나 PostgreSQL의 partitioned table과 PostgreSQL partition),table(SQLiteWITHOUT ROWID나 virtual table, primary key가 없는 table)이다.DIA-152 빠진 객체를 참조하는 객체도 보고하고 뺀다. 미지원 column 위의 index나 key, 빠진 table로 가는 foreign key, 미지원 column을 쓰는 check가 그렇다. introspect한 문서는 parse되며, parse가 거부한 줄의 객체는 그 diagnostic을 이유로 보고되고 빠진다. 문서가 parse될 때까지 반복한다. parse가 table을 거부하면 그 table은 모든 객체와 함께 빠지고, 같은 parse에서 거부된 그 table의 객체는 따로 보고하지 않는다. 각 객체는 한 번만 보고한다. catalog row가 column마다 하나인 foreign key도 그렇다.
DIA-153 SQLite는 table 항목을
CREATE TABLEtext에서 읽는다. column 정의는 renderer 형식, 곧 따옴표 친 이름, 선언 type,NULL이나NOT NULL, 있으면 default일 때 읽는다. 다른 clause(CHECK,REFERENCES,UNIQUE,COLLATE등)가 있는 column은 미지원이다. renderer 형식이 아닌CHECK,UNIQUE,FOREIGN KEY항목은 그 kind와 constraint 이름을 붙여 보고하며, 이름이 없으면 빈 이름이다.UNIQUE항목이나 clause의 자동 index는 따로 보고하지 않는다. 다른 형식의 primary key 항목과 그 밖의 항목은 table을 빼게 한다.DIA-191 CHECK가 없는 column을 포함해, declared type이 dbspec type이 없는 SQLite column은
declared type <type> with CHECK <check> has no dbspec type이유로 보고된다. 여기서<check>는 column의 CHECK text를 JSON 문자열로 쓴 것이고, CHECK가 없으면""이다. 여섯 client 모두 CHECK가 없는 column에 이 빈 문자열 형식을 정확히 사용한다. Go client는%q로, TypeScript client는JSON.stringify로<check>를 쓰며, Python client도 같은 text를 쓴다.DIA-198 introspection의 catalog query 수는 dialect별로 고정되어 table 수에 좌우되지 않는다.
Markdown IR executor
- DIA-154 DSN scheme
markdown://은 SQL dialect 대신 flowmark IR executor를 고른다(protocol). executor는 SQL text를 만들지 않으므로, 이 client의 각 표면은 여기 적은 규칙으로 지원되거나, 그 행의 코드로 거부된다. 이 표는tests/markdown/flowmark.version의 판에 있는 flowmark spec 60의 capability 표를 그대로 옮긴 것으로, 행마다 판정을 첫 최상위;,:,—,.에서 나누어 결과와 규칙으로 둔다.make markdown-table-check가 둘을 비교해 다르면 실패한다.
| 표면 | 결과 | 규칙 |
|---|---|---|
one, all, count, group_count, sum, avg | memory의 corpus 위에서 지원 | group_count는 NULL을 함께 묶는다 |
| 정렬 | IR order는 column의 type으로 정렬하며, NULL은 오름차순에서 모든 값 앞에, 내림차순에서 모든 값 뒤에 오고, 동률은 저장 순서다 | group_count의 group과 limit_per_parent의 순위에도 같다. 없는 읽기는 저장 순서를 돌려준다(document model). 근거: MySQL과 SQLite는 오름차순에서 NULL을 처음에, PostgreSQL은 마지막에 둔다. MySQL에는 NULLS FIRST가 없으므로, NULL을 가장 작게 두는 것이 세 database가 모방 없이 render하는 유일한 규칙이다 |
group_by | count와 group_count의 root에서만 유효하며, 그 밖에서는 IR_INVALID다 | 그것이 있으면 order 항목은 group column, 그 column 함수, 또는 random이어야 한다(아니면 IR_INVALID) — orm의 규칙. 근거: 모든 행 읽기는 primary key를 고르므로, group된 행 읽기는 MySQL 1055, PostgreSQL 42803, SQLite에서는 임의의 행이며, group되지 않은 column으로의 정렬도 같다 |
paginate | 지원 | 요청 하나가 일관된 corpus 하나를 읽는다. canonical root page limit은 ORM의 offset_p, count_p i64 parameter를 사용하며 page 행에 적용하고 count는 여전히 모든 일치 행을 센다. 잘못된 parameter 값은 corpus를 읽기 전에 실패한다 |
insert(단일, 다중 행) | 지원 | identity, key_prefix, order column은 executor가 할당하며 생략할 수 있다. default는 명시적으로 쓴다. insert는 값이나 NULL로 배정한다 — plus_p와 minus_p는 거기서 IR_INVALID이고 on_duplicate에서는 유효하다(orm의 규칙). 근거: MySQL은 그런 배정을 default에서 계산하고, PostgreSQL과 SQLite는 거절한다 |
update | 지원 | 읽은 뒤 바뀐 대상은 덮어쓰지 않고 WRITE_CONFLICT로 실패한다. primary key나 identity column의 배정은 IR_INVALID다(document model) |
delete, restore | 지원 | restore는 SQL 형태를 지키며(soft delete column을 비우고, 지운 동안 unique key를 유지), 그 key는 request hook 뒤에 orm의 ir.RestoreKey(orm protocol 1.5)가 set 값과 and로 이은 eq 조건에서 고르므로, hook의 조건은 update를 좁히듯 restore를 좁힌다. 참조되는 행의 delete는 같은 delete가 참조하는 행을 지우더라도 FOREIGN_KEY(constraints). primary key, identity, soft delete column을 배정하는 restore는 IR_INVALID다 |
optimistic | 지원 | 일치가 없으면 OPTIMISTIC_LOCK |
on_duplicate | insert된 행과 만날 수 있는 key | insert가 identity나 key_prefix에 맡기지 않는 한 primary key, 그리고 각 unique key — 가 정확히 하나이고 insert가 그 모든 column을 배정할 때 지원한다. 그 key가 충돌 대상이며, 그 밖의 on_duplicate는 IR_INVALID다(MySQL의 아무 key 규칙과 PostgreSQL과 SQLite의 충돌 대상을 같은 key로 만드는 orm의 규칙). primary key는 아래 비교가 정하는 대로 행과 만난다. 근거: MySQL은 자신의 key 순서로 아무 unique key에서나 행과 만나는 반면, PostgreSQL과 SQLite는 충돌 대상 하나를 지정하고 다른 key에서의 만남은 DUPLICATE_KEY다. 행과 만날 수 있는 key가 하나이면 셋은 구성상 같다 |
| primary key 비교 | A–Z의 대소문자를 가리지 않는다 | SQL에서 orm의 binary collation과 다른데, key는 file 이름이자 anchor이기 때문이다(document model): key가 다른 key와 대소문자를 무시하고 같은 insert는 DUPLICATE_KEY이며, 충돌 key가 primary key인 on_duplicate insert는 그 행 — 바이트 단위로 같은 key가 먼저, 아니면 byte 순서에서 가장 작은 것 — 과 만나고, 그 행은 자기 key를 유지한다. 근거: ON DUPLICATE KEY UPDATE와 ON CONFLICT는 충돌 key의 index가 그 index의 비교로 보고하는 중복에서 작동한다 |
lock update, share | 문서화된 no-op으로 받는다 | server transaction이 이미 corpus lock을 잡고 있다 |
lock update_nowait, share_nowait | CAPABILITY_UNSUPPORTED | |
relation(limit_per_parent, if_parent, flatten, key_by, no_cascade_delete) | 지원 | executor가 조립하며, flatten relation은 one relation으로 답하고 client model이 그것을 부모의 array와 JSON 형태에 병합한다(orm의 규칙). key_by는 non-null column이어야 한다(아니면 IR_INVALID). 근거: orm은 병합을 client model의 array와 JSON 형태에 두며, 부모가 가진 이름은 부모의 값을 유지한다. 병합된 답은 client에게 부모 자신의 column과 구별할 수 없는 행을 넘긴다. NULL collection key는 client 언어마다 다르게 답하므로, nullable key_by는 형태로 거절한다 |
subquery(tuple_in, tuple_not_in, scalar Sub aggregate) | 지원 | |
scalar column subquery(agg 없는 columns.sub) | limit count 1과, 그 entity의 모든 primary key column을 담고 random이 없는 order가 있을 때 유효하며, 아니면 IR_INVALID다(orm의 규칙) | 값은 그 행의 column이며, 일치하는 행이 없으면 NULL이다. 근거: 여러 행의 scalar subquery는 MySQL 1242, PostgreSQL 21000, SQLite에서는 첫 행이므로, 한 행을 내는 형태만이 하나의 결과를 가진다 |
column 비교(*_col, subquery의 in, join left/right, relation key) | 두 column은 dbspec의 check operand처럼 만난다 | 정수끼리, scale이 같은 decimal, f64, varchar와 text, 정밀도가 같은 time이나 datetime, 그리고 uuid, date, bool, bytes는 각각 자신과. 다른 쌍은 OPERATOR_NOT_ALLOWED다(orm의 규칙). 근거: PostgreSQL은 integer = varchar를 거절하고, MySQL은 변환하며, SQLite는 affinity로 비교한다. date = datetime, scale이 다른 decimal, 2^53 근처의 정수와 f64도 세 database에서 갈린다 |
contains(Lk 연산자) | 지원 | column의 텍스트가 parameter의 텍스트를 substring으로 담으면 참이다. ASCII 문자 A–Z와 a–z는 대소문자를 가리지 않고, 그 밖의 문자는 code point로 비교한다. NULL column이나 parameter는 unknown이며, parameter의 %, _, \는 문자 그대로다. 근거: SQL parity — orm의 COLLATE "C" 아래 PostgreSQL ILIKE, SQLite LIKE, orm의 MySQL ASCII fold가 이 결과를 준다 |
contains_binary(Lb 연산자) | 지원 | column의 텍스트가 parameter의 텍스트를 substring으로 담으면 참이며, code point로 비교한다. NULL column이나 parameter는 unknown이며, parameter의 %, _, \는 문자 그대로다. 근거: utf8mb4_0900_bin 아래 MySQL LIKE, COLLATE "C" 아래 PostgreSQL LIKE, SQLite instr가 일치한다 |
now, default now, updated, soft_delete | 요청마다 값 하나인 요청의 now | datetime(p)에 쓸 때는 p자리로 자르고, 비교할 때는 여섯 자리다(operations). 결과가 0001-01-01부터 9999-12-31 23:59:59.999999 밖에 있거나 간격을 표현할 수 없는 상대 시각 함수는 NULL이다(orm의 규칙). 근거: MySQL과 SQLite는 범위를 넘으면 NULL을 주며, NULL은 다른 dialect를 guard로 맞출 수 있는 유일한 결과다 |
soft_delete | 모든 읽기는 soft delete된 행을 제외한다 | root, 각 join 자식(join 조건에서 제외하므로 left join은 null을 묶는다), 각 relation과 각 subquery. restore만이 그런 행에 일치한다. 근거: 읽기는 늘 soft delete된 행을 제외한다는 orm의 규칙 |
| codec columns | orm 요청에서는 client가 적용한다 | 엔진이 시작하는 쓰기(CLI, MCP, sidecar, 이력)는 orm의 Go codec package로 codec을 적용하며, 검증은 같은 package로 decode한다. 저장 값은 encode된 값이며, byte를 만드는 단계는 base64다 |
| indexes | 허용되며 메모리에서 파생된다 | |
select explicit, entity, navigation | client 쪽이며 저장에 효과가 없다 | |
aes, aes_version, blind_index | corpus에서 schema 오류 | key는 client를 떠나지 않는다 |
audit, immutable setting | corpus에서 schema 오류 | 대신 state_machine … history |
| transaction | session 위의 begin부터 commit/rollback까지 | readOnly는 지키며 lock을 잡는다 |
| savepoint | 중첩 stage | |
orderByRandom | 지원 | 순서에 seed를 쓰지 않는다 |
| audit 값을 가진 transaction | CAPABILITY_UNSUPPORTED | |
isolation, timeoutMs option | CAPABILITY_UNSUPPORTED | |
getQuery(), statement event, subscribe() | CAPABILITY_UNSUPPORTED | SQL 텍스트가 없다 |
force_index | CAPABILITY_UNSUPPORTED | index는 이름을 지정하지 않고 파생된다 |
fulltext | OPERATOR_NOT_ALLOWED | SQLite는 table에 full-text index가 없다(orm dialects.md) |
| lock, local setting, privilege, AES rotation utility | CAPABILITY_UNSUPPORTED | |
| schema install, dbplan chain | CAPABILITY_UNSUPPORTED | flowmark install이 corpus를 만들고(schema), 진화는 migration을 따른다 |
사용할 수 없는 table(format.block, format.section, format.encoding이나 그 행 사이의 parent.cycle이 있는 file, 바이트 단위로 같은 key를 가진 두 행, 또는 자리의 corpus.walk 항목) | 그 행이 필요한 모든 요청은 읽기든 쓰기든 file과 진단을 밝히며 CONSTRAINT로 실패하며, 다른 file들의 행만으로 답하지 않는다(canonical format) |
- DIA-155
one,all,count,group_count,sum,avg— 결과: memory의 corpus 위에서 지원; 규칙:group_count는 NULL을 함께 묶는다 - DIA-156 정렬 — 결과: IR
order는 column의 type으로 정렬하며, NULL은 오름차순에서 모든 값 앞에, 내림차순에서 모든 값 뒤에 오고, 동률은 저장 순서다; 규칙:group_count의 group과limit_per_parent의 순위에도 같다. 없는 읽기는 저장 순서를 돌려준다(document model). 근거: MySQL과 SQLite는 오름차순에서 NULL을 처음에, PostgreSQL은 마지막에 둔다. MySQL에는NULLS FIRST가 없으므로, NULL을 가장 작게 두는 것이 세 database가 모방 없이 render하는 유일한 규칙이다 - DIA-157
group_by— 결과:count와group_count의 root에서만 유효하며, 그 밖에서는IR_INVALID다; 규칙: 그것이 있으면order항목은 group column, 그 column 함수, 또는random이어야 한다(아니면IR_INVALID) — orm의 규칙. 근거: 모든 행 읽기는 primary key를 고르므로, group된 행 읽기는 MySQL 1055, PostgreSQL 42803, SQLite에서는 임의의 행이며, group되지 않은 column으로의 정렬도 같다 - DIA-158
paginate— 결과: 지원; 규칙: 요청 하나가 일관된 corpus 하나를 읽는다. canonical root page limit은 ORM의offset_p,count_pi64 parameter를 사용하며 page 행에 적용하고count는 여전히 모든 일치 행을 계산한다. 잘못된 parameter 값은 corpus를 읽기 전에 실패한다 - DIA-159
insert(단일, 다중 행) — 결과: 지원; 규칙:identity,key_prefix,ordercolumn은 executor가 할당하며 생략할 수 있다. default는 명시적으로 쓴다. insert는 값이나 NULL로 배정한다 —plus_p와minus_p는 거기서IR_INVALID이고on_duplicate에서는 유효하다(orm의 규칙). 근거: MySQL은 그런 배정을 default에서 계산하고, PostgreSQL과 SQLite는 거절한다 - DIA-160
update— 결과: 지원; 규칙: 읽은 뒤 바뀐 대상은 덮어쓰지 않고WRITE_CONFLICT로 실패하며, transaction이 읽은 뒤의 변경 때문에 쓰기가 다른 쓰기가 되는 commit도 그렇다. 클라이언트는DEADLOCK과 달리WRITE_CONFLICT를 재시도하지 않는다. primary key나 identity column의 배정은IR_INVALID다(document model) - DIA-161
delete,restore— 결과: 지원; 규칙:restore는 SQL 형태를 지키며(soft delete column을 비우고, 지운 동안 unique key를 유지), 그 key는 request hook 뒤에 orm의ir.RestoreKey(orm protocol 1.5)가 set 값과and로 이은eq조건에서 고르므로, hook의 조건은 update를 좁히듯 restore를 좁힌다. 참조되는 행의 delete는 같은 delete가 참조하는 행을 지우더라도FOREIGN_KEY(constraints). primary key, identity, soft delete column을 배정하는restore는IR_INVALID다 - DIA-162
optimistic— 결과: 지원; 규칙: 일치가 없으면OPTIMISTIC_LOCK - DIA-163
on_duplicate— 결과: insert된 행과 만날 수 있는 key; 규칙: insert가identity나key_prefix에 맡기지 않는 한 primary key, 그리고 각 unique key — 가 정확히 하나이고 insert가 그 모든 column을 배정할 때 지원한다. 그 key가 충돌 대상이며, 그 밖의on_duplicate는IR_INVALID다(MySQL의 아무 key 규칙과 PostgreSQL과 SQLite의 충돌 대상을 같은 key로 만드는 orm의 규칙). primary key는 아래 비교가 정하는 대로 행과 만난다. 근거: MySQL은 자신의 key 순서로 아무 unique key에서나 행과 만나는 반면, PostgreSQL과 SQLite는 충돌 대상 하나를 지정하고 다른 key에서의 만남은DUPLICATE_KEY다. 행과 만날 수 있는 key가 하나이면 셋은 구성상 같다 - DIA-164 primary key 비교 — 결과: A–Z의 대소문자를 가리지 않는다; 규칙: SQL에서 orm의 binary collation과 다른데, key는 file 이름이자 anchor이기 때문이다(document model): key가 다른 key와 대소문자를 무시하고 같은 insert는
DUPLICATE_KEY이며, 충돌 key가 primary key인on_duplicateinsert는 그 행 — 바이트 단위로 같은 key가 먼저, 아니면 byte 순서에서 가장 작은 것 — 과 만나고, 그 행은 자기 key를 유지한다. 근거:ON DUPLICATE KEY UPDATE와ON CONFLICT는 충돌 key의 index가 그 index의 비교로 보고하는 중복에서 작동한다 - DIA-165
lockupdate,share— 결과: 문서화된 no-op으로 받는다; 규칙: server transaction이 이미 corpus lock을 잡고 있다 - DIA-166
lockupdate_nowait,share_nowait— 결과:CAPABILITY_UNSUPPORTED - DIA-167 relation(
limit_per_parent,if_parent,flatten,key_by,no_cascade_delete) — 결과: 지원; 규칙: executor가 조립하며,flattenrelation은onerelation으로 답하고 client model이 그것을 부모의 array와 JSON 형태에 병합한다(orm의 규칙).key_by는 non-null column이어야 한다(아니면IR_INVALID). 근거: orm은 병합을 client model의 array와 JSON 형태에 두며, 부모가 가진 이름은 부모의 값을 유지한다. 병합된 답은 client에게 부모 자신의 column과 구별할 수 없는 행을 넘긴다. NULL collection key는 client 언어마다 다르게 답하므로, nullablekey_by는 형태로 거절한다 - DIA-168 subquery(
tuple_in,tuple_not_in, scalarSubaggregate) — 결과: 지원 - DIA-169 scalar column subquery(
agg없는columns.sub) — 결과:limitcount 1과, 그 entity의 모든 primary key column을 담고random이 없는order가 있을 때 유효하며, 아니면IR_INVALID다(orm의 규칙); 규칙: 값은 그 행의 column이며, 일치하는 행이 없으면 NULL이다. 근거: 여러 행의 scalar subquery는 MySQL 1242, PostgreSQL 21000, SQLite에서는 첫 행이므로, 한 행을 내는 형태만이 하나의 결과를 가진다 - DIA-170 column 비교(
*_col, subquery의in, joinleft/right, relation key) — 결과: 두 column은 dbspec의 check operand처럼 만난다; 규칙: 정수끼리, scale이 같은decimal,f64,varchar와text, 정밀도가 같은time이나datetime, 그리고uuid,date,bool,bytes는 각각 자신과. 다른 쌍은OPERATOR_NOT_ALLOWED다(orm의 규칙). 근거: PostgreSQL은integer = varchar를 거절하고, MySQL은 변환하며, SQLite는 affinity로 비교한다.date = datetime, scale이 다른 decimal, 2^53 근처의 정수와f64도 세 database에서 갈린다 - DIA-171
contains(Lk연산자) — 결과: 지원; 규칙: column의 텍스트가 parameter의 텍스트를 substring으로 담으면 참이다. ASCII 문자 A–Z와 a–z는 대소문자를 가리지 않고, 그 밖의 문자는 code point로 비교한다. NULL column이나 parameter는 unknown이며, parameter의%,_,\는 문자 그대로다. 근거: SQL parity — orm의COLLATE "C"아래 PostgreSQLILIKE, SQLiteLIKE, orm의 MySQL ASCII fold가 이 결과를 준다 - DIA-172
contains_binary(Lb연산자) — 결과: 지원; 규칙: column의 텍스트가 parameter의 텍스트를 substring으로 담으면 참이며, code point로 비교한다. NULL column이나 parameter는 unknown이며, parameter의%,_,\는 문자 그대로다. 근거:utf8mb4_0900_bin아래 MySQLLIKE,COLLATE "C"아래 PostgreSQLLIKE, SQLiteinstr가 일치한다 - DIA-173
now,default now,updated,soft_delete— 결과: 요청마다 값 하나인 요청의now; 규칙:datetime(p)에 쓸 때는 p자리로 자르고, 비교할 때는 여섯 자리다(operations). 결과가 0001-01-01부터 9999-12-31 23:59:59.999999 밖에 있거나 간격을 표현할 수 없는 상대 시각 함수는 NULL이다(orm의 규칙). 근거: MySQL과 SQLite는 범위를 넘으면 NULL을 주며, NULL은 다른 dialect를 guard로 맞출 수 있는 유일한 결과다 - DIA-174
soft_delete— 결과: 모든 읽기는 soft delete된 행을 제외한다; 규칙: root, 각 join 자식(join 조건에서 제외하므로 left join은 null을 묶는다), 각 relation과 각 subquery.restore만이 그런 행에 일치한다. 근거: 읽기는 늘 soft delete된 행을 제외한다는 orm의 규칙 - DIA-175 codec columns — 결과: orm 요청에서는 client가 적용한다; 규칙: 엔진이 시작하는 쓰기(CLI, MCP, sidecar, 이력)는 orm의 Go codec package로 codec을 적용하며, 검증은 같은 package로 decode한다. 저장 값은 encode된 값이며, byte를 만드는 단계는 base64다
- DIA-176 indexes — 결과: 허용되며 메모리에서 파생된다
- DIA-177
select explicit,entity,navigation— 결과: client 쪽이며 저장에 효과가 없다 - DIA-178
aes,aes_version,blind_index— 결과: corpus에서 schema 오류; 규칙: key는 client를 떠나지 않는다 - DIA-179
audit,immutablesetting — 결과: corpus에서 schema 오류; 규칙: 대신state_machine … history - DIA-180 transaction — 결과: session 위의
begin부터commit/rollback까지; 규칙:readOnly를 지킨다: 클라이언트는read_onlytrue인begin을 쓰고, executor는 그 transaction에 쓰기 lock을 잡지 않으며 그 안의 모든 쓰기를READ_ONLY로 거절한다 - DIA-181 savepoint — 결과: 중첩 stage
- DIA-182
orderByRandom— 결과: 지원; 규칙: 순서에 seed를 쓰지 않는다 - DIA-183 audit 값을 가진 transaction — 결과:
CAPABILITY_UNSUPPORTED - DIA-184
isolation,timeoutMsoption — 결과:CAPABILITY_UNSUPPORTED - DIA-185
getQuery(), statement event,subscribe()— 결과:CAPABILITY_UNSUPPORTED; 규칙: SQL 텍스트가 없다 - DIA-186
force_index— 결과:CAPABILITY_UNSUPPORTED; 규칙: index는 이름을 지정하지 않고 파생된다 - DIA-187
fulltext— 결과:OPERATOR_NOT_ALLOWED; 규칙: SQLite는 table에 full-text index가 없다(orm dialects.md) - DIA-188 lock, local setting, privilege, AES rotation utility — 결과:
CAPABILITY_UNSUPPORTED - DIA-189 schema install, dbplan chain — 결과:
CAPABILITY_UNSUPPORTED; 규칙:flowmark install이 corpus를 만들고(schema), 진화는 migration을 따른다 - DIA-190 사용할 수 없는 table(
format.block,format.section,format.encoding이나 그 행 사이의parent.cycle이 있는 file, 바이트 단위로 같은 key를 가진 두 행, 또는 자리의corpus.walk항목) — 결과: 그 행이 필요한 모든 요청은 읽기든 쓰기든 file과 진단을 밝히며CONSTRAINT로 실패하며, 다른 file들의 행만으로 답하지 않는다(canonical format)
Notifications
- DIA-192 정의.
notify(channel, payload),listen(channel),unlisten(channel)은 PostgreSQL dialect의 클라이언트 작업이다: PostgreSQL의pg_notify와LISTEN/UNLISTEN. IR이 아니므로 planner나 executor가 만들지 않는다(protocol). channel은 U+0000 없는 UTF-8 1~63바이트의 PostgreSQL 식별자이며, 그 범위 밖의 channel은 문을 실행하기 전에CONFIG로 실패한다. - DIA-193 notify.
notify는 자기 handle의 연결에서SELECT pg_notify($1, $2)를 실행한다. transaction 안에서는 알림이 그 transaction이 commit될 때 전달되고 rollback되면 버려지며, 밖에서는 자기 문의 commit에 전달된다. PostgreSQL payload는 8000바이트보다 짧아야 한다. UTF-8 8000바이트 이상이거나 U+0000을 포함한 payload는 문을 실행하기 전에DRIVER로 실패한다. - DIA-194 listen.
listen(channel)은 pool 밖의 자기 연결을 열고, channel을 인용된 식별자로LISTEN을 실행하며, 알림마다 channel, payload, 보낸 프로세스의 pid를 준다. 받기 대기가 없을 때 도착한 알림은 도착 순서대로 보관한다. 연결이 끊기면 구독은CONNECTION_LOST로 끝난다. - DIA-195 unlisten과 취소.
unlisten(channel)은 listen 연결에서UNLISTEN을 실행하고 그 channel의 전달을 멈춘다.close()는 구독의 모든 channel에UNLISTEN을 실행하고 연결을 클라이언트에 오류 없이 돌려준다. 대기 중인 받기는 CFG-9가 정하는 대로 그 언어의 취소 수단으로 취소한다: Go는db.WithContext(ctx)의 context를, TypeScript는db.withSignal(signal)의 signal을 취소한다. Python, PHP, Rust에는 받기를 위한 그런 수단이 없으므로 그 받기는 기한을 받으며, 기한이 지나면 받기가 끝난다. 취소되었거나 기한이 지난 받기는CANCELED로 실패하고 구독을 닫으며, 그 뒤의 호출은CONFIG로 실패한다. - DIA-196 다른 데이터베이스. MySQL, SQLite, markdown executor에서
notify,listen,unlisten은 문이나 연결을 쓰기 전에CAPABILITY_UNSUPPORTED로 실패한다. 어느 것도 조용한 no-op이 아니다. - DIA-197 Python 드라이버 형식: Python client는 엔진이 쓴 문장을 자기 드라이버 형식으로 바꿔 전송한다. MySQL의
?표지는%s가 되고 값을 순서대로 받는다. PostgreSQL의$n표지는 이름pn을 가진%(pn)s가 된다. 인용 문자열, 인용 식별자, 주석 안의 표지는 글자이므로 그대로 둔다. 드라이버가 문장 전체를 형식화하므로 문장의 모든%는%%로 쓴다. 값이 없는 문장은 인자 없이 그대로 드라이버에 넘긴다.