Skip to content

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 a parent slot 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 off

  • DIA-4 Lk — MySQL 8.0.2+ / MariaDB 10.2+: F(col) LIKE F(?), F는 A–Z 각각을 소문자로 바꾸는 REPLACE 26겹: column의 utf8mb4_0900_bin은 code point로 비교하고, LOWER나 _ci collation은 ASCII 밖까지 접는다(probe mysql.like.binary_collation_case_sensitive, mysql.like.ascii_fold_replace); PostgreSQL 12+: COLLATE "C" 아래의 ILIKE(probe postgres.ilike.c_collation_ascii_only); SQLite 3.46+: LIKE … ESCAPE '\' (ASCII 대소문자 구분 없음, probe sqlite.like.ascii_only)

  • DIA-5 Lb — MySQL 8.0.2+ / MariaDB 10.2+: LIKE (binary collation utf8mb4_0900_bin, code point로 비교한다. LIKE BINARY는 MySQL 8.0.27부터 deprecated다. probe mysql.like.binary_collation_code_point); PostgreSQL 12+: LIKE; SQLite 3.46+: instr(col, ?) > 0

  • DIA-6 Fulltext / FulltextBoolean — MySQL 8.0.2+ / MariaDB 10.2+: MATCH … AGAINST (… IN NATURAL LANGUAGE/BOOLEAN MODE), boolean value normalized by the transform fulltext_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 with IR_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 adds pk = LAST_INSERT_ID(pk)); PostgreSQL 12+: RETURNING pk; SQLite 3.46+: RETURNING pk

  • DIA-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 vector upsert_one_key는 세 database에서 같은 결과를 준다

  • DIA-10 groupLimit(n) — MySQL 8.0.2+ / MariaDB 10.2+: ROW_NUMBER() OVER (…); PostgreSQL 12+: same; SQLite 3.46+: same

  • DIA-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/hex styles — 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 ip style — 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 hex column은 MySQL에서도 주소를 client에서 pack하고 unpack하며(16-byte packed 뒤 hex text), 그 placeholder는 저장되는 hex text를 받는다(planner case planner_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로 자른 텍스트(now bind slot); SQLite 3.46+: executor가 bind한, p로 자른 텍스트(now bind 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한다.

go
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는 now bind 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:SS text이고 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 vector datetime_p0_roundtrip과 time_p3_roundtrip이 그런 값을 삽입하고 같은 매개변수로 다시 읽어 비교하고, planner case planner_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는 PostgreSQL uuid 컬럼을 CAST(col AS text)로 읽고 값을 CAST(CAST(? AS text) AS uuid)로 bind한다(conformance vector uuid_roundtrip).
  • DIA-28 MySQL은 TEXT, BLOB, JSON, geometry 컬럼의 리터럴 기본값을 식 형식 DEFAULT ('value')로 기록하며 MySQL 8.0.13 이상이 필요하다. import는 이를 리터럴로 다시 읽는다.
  • DIA-29 Boolean은 PostgreSQL boolean, MySQL BOOLEAN/TINYINT(1)의 숫자 0/1 기본값, SQLite INTEGER 0/1을 사용한다. 정수가 아니라 boolean을 bind한다.
  • DIA-30 CHECK 표현식은 PostgreSQL과 SQLite에서 선언한 표현식을 그대로 생성한다. MySQL은 predicate를 반환하는 CASE CHECK 정의를 거부하므로 명시적인 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, now slot은 datetime이다. parent slot은 비교하는 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를 그것이 채우는 param slot의 논리 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\0 authenticated ciphertext format, 12바이트 random nonce, AES-256-GCM, version key derivation을 사용한다.
  • DIA-33 AES column이 있는 entity는 aes_version setting으로 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), PostgreSQL COLLATE "C" 아래의 ILIKE와 SQLite LIKE는 ASCII 글자만 접는다. conformance vector contains_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 vector relative_time_out_of_range가 각 비교의 양쪽을 실행한다. 월 연산은 대상 월의 마지막 유효한 날을 유지한다(DSL-142). MySQL DATE_ADD와 DATE_SUB, PostgreSQL make_interval(months => n)은 스스로 그렇게 하고(mysql.relative.month_end_kept, postgres.relative.month_end_kept), SQLite datetime은 대상 월의 끝을 넘는 날을 다음 달로 넘기므로 그 형태에 floor modifier를 더한다(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 ​

항목값
MySQL8.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)
PostgreSQL17.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 clientmodernc.org/sqlite v1.58.0(packages/go/go.mod), select sqlite_version() 결과 3.53.4
SQLite, PHP clientPHP 8.4.26의 pdo_sqlite, select sqlite_version() 결과 3.53.4
SQLite, TypeScript clientNode 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 clientsqlx 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로 지원한다. MySQL SMALLINT/INT/BIGINT, PostgreSQL smallint/integer/bigint, SQLite smallint/integer/bigint와 renderer CHECK typeof(c) IN ('integer','null') AND c BETWEEN min AND max(i64는 범위 없음). 8비트·24비트 정수는 PostgreSQL에 해당 타입이 없으므로 지원하지 않는다; Introspection: MySQL COLUMNS.COLUMN_TYPE, PostgreSQL pg_attribute.atttypid/format_type, SQLite pragma_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: MySQL COLUMN_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로 지원한다. MySQL tinyint(1)과 renderer CHECK b IN (0,1), PostgreSQL boolean, SQLite BOOLEAN과 renderer CHECK b IN (0,1). 필드 이름은 boolean을 선택하지 않는다; Introspection: MySQL COLUMN_TYPE tinyint(1)과 CHECK, PostgreSQL boolean, 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)로 지원한다. MySQL DECIMAL(p,s), PostgreSQL numeric(p,s), SQLite는 값 × 10^s를 저장하는 DECIMALINT(p,s)와 renderer CHECK typeof(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: MySQL NUMERIC_PRECISION/NUMERIC_SCALE, PostgreSQL numeric_precision/numeric_scale, SQLite 선언 타입 텍스트
  • DIA-42 부동소수점 — MySQL 8.4: FLOAT는 4바이트, DOUBLE은 8바이트. NaN과 무한대 값이 없다(1367); PostgreSQL 17: real 4바이트, double precision 8바이트. 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를 지원한다. MySQL DOUBLE, PostgreSQL double precision, SQLite REAL. 값은 유한하며 클라이언트가 NaN과 무한대를 거부한다. SQLite에 4바이트 float가 없으므로 f32는 지원하지 않는다; Introspection: MySQL DATA_TYPE, PostgreSQL atttypid, 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과 PostgreSQL varchar(n), SQLite varchar(n)과 renderer CHECK length(v) <= n. 클라이언트는 뒤쪽 공백을 포함해 n자를 넘는 값을 어떤 statement보다 먼저 CODEC_ENCODE로 거부하므로(conformance vector varchar_length_rejected) 데이터베이스의 공백 제거는 쓰지 않는다; Introspection: MySQL CHARACTER_MAXIMUM_LENGTH, PostgreSQL atttypmod/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로 지원한다. MySQL LONGTEXT, PostgreSQL text, SQLite TEXT. text 컬럼은 key나 index에 포함할 수 없다. 최대 값은 PostgreSQL 1 GB 필드 한도, SQLite SQLITE_MAX_LENGTH, MySQL max_allowed_packet 중 가장 작은 값이다. Ordered JSON은 codec setting을 가진 text다; Introspection: MySQL DATA_TYPE, PostgreSQL atttypid, 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 vector text_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를 지원한다. MySQL LONGBLOB, PostgreSQL bytea, SQLite BLOB. 채움과 길이는 MySQL에만 있으므로 binary(n)과 varbinary(n)은 지원하지 않는다; Introspection: MySQL DATA_TYPE, PostgreSQL atttypid, 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로 지원한다. PostgreSQL uuid, MySQL char(36) CHARACTER SET ascii COLLATE ascii_bin과 renderer CHECK REGEXP_LIKE(u, '^[0-9a-f]{8}-…$', 'c'), SQLite TEXT와 문자 class 36개의 renderer CHECK u GLOB. 클라이언트는 소문자 표준 텍스트를 쓰고 다른 형식은 어떤 statement보다 먼저 CODEC_ENCODE로 거부하므로(conformance vector uuid_form_rejected) PostgreSQL의 다른 입력 형식 변환은 쓰지 않는다; Introspection: PostgreSQL uuid, 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은 지원하지 않는다. 값 목록은 in CHECK 정의를 가진 varchar(n) 컬럼이며 모든 데이터베이스에서 텍스트로 정렬한다; Introspection: MySQL ENUM 컬럼과 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: MySQL JSON, PostgreSQL json/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로 지원한다. MySQL DATE, PostgreSQL date, SQLite DATE와 renderer CHECK d IS date(d); Introspection: MySQL DATA_TYPE, PostgreSQL atttypid, 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)로 지원한다. MySQL TIME(p)와 renderer CHECK v >= '00:00:00' AND v < '24:00:00', PostgreSQL time(p)와 renderer CHECK v < '24:00:00', SQLite TIME 텍스트와 p = 0의 renderer CHECK v 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 vector datetime_precision_rejected) 데이터베이스 반올림은 쓰지 않는다; Introspection: MySQL DATETIME_PRECISION, PostgreSQL datetime_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)로 지원한다. MySQL DATETIME(p), PostgreSQL timestamp(p), SQLite는 소수 p자리의 YYYY-MM-DD HH:MM:SS 텍스트 DATETIME과 renderer CHECK(길이, datetime(), 24 미만의 시, 숫자). 모든 연결은 UTC로 읽고 쓴다. 시간대 정보가 있는 시점(MySQL TIMESTAMP, PostgreSQL timestamptz)은 지원하지 않는다; Introspection: MySQL DATA_TYPE, PostgreSQL atttypid(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로 자른 now slot을, now value function에 여섯 자리 slot을 bind한다(postgres.now.compare_six_digits). SQLite도 now slot을 같은 방식으로 bind하고 clock과 비교하는 datetime(p < 6) 컬럼을 여섯 자리로 늘린다; Introspection: 기본값 텍스트(기본값 참조)

시간대 결정(owner: UTC 연결 규칙을 가진 local date-time):

  • DIA-57 정의. datetime(p)는 시간대 정보가 없는 wall clock date-time이다. 실행 규칙은 모든 연결이 UTC로 읽고 쓰는 것이다. 클라이언트는 각 연결에서 MySQL time_zone='+00:00', PostgreSQL TimeZone='UTC'를 설정하고, offset이 있는 값은 bind 전에 UTC로 변환하며, offset 리터럴을 보내지 않는다. SQLite는 클라이언트가 쓴 UTC 텍스트 형식을 저장한다. 시간대 없는 date-time을 주는 driver(PyMySQL, psycopg)는 연결 시간대의 wall clock을 주므로, 클라이언트는 그 값을 연결 시간대로 읽으며 process의 지역 시간대로 읽지 않는다. 그래서 모든 읽기와 optimistic updated_ts 비교의 결과는 process의 시간대와 무관하다. 프로그램이 bind로 넘기는 시간대 없는 date-time, 곧 Rust NaiveDateTime과 같은 시간대 없는 Python datetime도 연결 시간대(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 규칙에서 시각 함수가 일치한다. MySQL NOW(6)는 UTC_TIMESTAMP(6)와 같고(mysql.render.datetime_utc_session), PostgreSQL CURRENT_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이 복원할 수 없는 MySQL DATETIME과 PostgreSQL timestamptz rendering이다.
  • 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). MySQL TIMESTAMP 컬럼은 +00:00 session에서 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: MySQL IS_NULLABLE, PostgreSQL attnotnull, SQLite pragma_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 형식을 리터럴로 정규화한다. MySQL text 리터럴은 식 형식을 쓴다; Introspection: MySQL COLUMN_DEFAULT와 EXTRA, PostgreSQL pg_attrdef와 pg_get_expr, SQLite pragma_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 생성기(MySQL uuid(), PostgreSQL gen_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: 생략하면 생성하는 단일 컬럼 부호 있는 i64 primary key로 지원한다. MySQL BIGINT AUTO_INCREMENT, PostgreSQL bigint GENERATED BY DEFAULT AS IDENTITY, SQLite INTEGER PRIMARY KEY AUTOINCREMENT. Key는 다시 쓰지 않는다. 클라이언트는 이 컬럼의 명시 값을 거부하며, PostgreSQL에서 명시 key를 쓰는 apply 단계는 identity sequence를 가장 큰 key로 맞춘다. 간격은 의미에 포함하지 않는다; Introspection: MySQL EXTRA auto_increment, PostgreSQL attidentity/identity_generation, SQLite sqlite_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: MySQL STATISTICS index PRIMARY, PostgreSQL pg_constraint contype='p', SQLite pragma_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: MySQL STATISTICS.NON_UNIQUE=0, PostgreSQL pg_index.indisunique와 pg_constraint, SQLite pragma_index_list.unique/origin
  • DIA-67 Key 순서 — MySQL 8.4: DESC를 저장한다(COLLATION D); 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: MySQL SUB_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_NAME NULL, 정규화한 EXPRESSION; PostgreSQL 17: 식 key(pg_index.indkey 0); 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: FULLTEXT index; 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: MySQL STATISTICS.INDEX_NAME, PostgreSQL index의 pg_class, SQLite pragma_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을 지원하며 명시 keyword RESTRICT, CASCADE, SET NULL로 생성한다. MySQL은 NO ACTION을 즉시 검사하므로 NO ACTION은 지원하지 않는다. 하나의 DELETE가 참조되는 행과 그 행을 참조하는 행을 함께 지우면, MySQL과 SQLite는 참조되는 행이 행 순서에서 먼저 나올 때 거부하고 PostgreSQL은 받아들인다; 이식 가능한 코드는 참조하는 행을 먼저 지운다.; Introspection: MySQL REFERENTIAL_CONSTRAINTS.UPDATE_RULE/DELETE_RULE, PostgreSQL pg_constraint.confupdtype/confdeltype, SQLite pragma_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: MySQL MATCH_OPTION, PostgreSQL confmatchtype, SQLite pragma_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, PHP PRAGMA, Node node: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: MySQL REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME, PostgreSQL pg_constraint.conname, SQLite sqlite_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: MySQL TABLE_CONSTRAINTS.ENFORCED, PostgreSQL convalidated
  • DIA-84 식 텍스트 — MySQL 8.4: CHECK_CLAUSE는 정규화한다: (`a` in (1,2)), 문자열 리터럴에 _utf8mb4 introducer와 두 번의 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: MySQL CHECK_CONSTRAINTS.CHECK_CLAUSE, PostgreSQL pg_get_constraintdef, SQLite sqlite_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_null foreign 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 collation C: 대소문자·악센트·공백을 구분하고 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 하나만 지원한다. MySQL CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin, PostgreSQL은 database 기본값과 관계없이 모든 text 컬럼에 COLLATE "C", SQLite BINARY. 다른 collation은 지원하지 않는다. postgres.collation.c_codepoint의 COLLATE "C" 컬럼 catalog 확인은 아직 실행하지 않았다; Introspection: MySQL COLUMNS.COLLATION_NAME, PostgreSQL pg_attribute.attcollation, SQLite sqlite_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: MySQL DATABASE(), PostgreSQL current_schema(), SQLite main

출처: 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: MySQL EXTRA on update …는 미지원으로 보고한다
  • DIA-96 Trigger — MySQL 8.4: BEFORE UPDATE row trigger는 NEW에 대입할 수 있고 값이 바뀌지 않아도 실행된다; PostgreSQL 17: 함수를 쓰는 BEFORE UPDATE row trigger는 NEW에 대입할 수 있고 값이 바뀌지 않아도 실행된다; SQLite: NEW에 대입할 수 없다. AFTER UPDATE trigger가 별도 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)로 남는다. MySQL ON 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: MySQL TRIGGERS.ACTION_TIMING/EVENT_MANIPULATION/ACTION_ORIENTATION, PostgreSQL pg_trigger.tgtype, SQLite sqlite_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은 DELETE trigger를 건너뛴다; PostgreSQL 17: BEFORE TRUNCATE statement 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_null foreign key의 자식 table에는 audit과 immutable setting을 거부한다. 따라서 모든 데이터베이스가 같은 행 변경을 관찰한다; 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: PostgreSQL pg_proc
  • DIA-105 NEW 대입 — MySQL 8.4: BEFORE trigger에서 허용; PostgreSQL 17: BEFORE row 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는 TEMP table을 읽도록 만들 수 있지만 실행할 때 실패한다(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에 복사한다. physical DELETE의 OLD는 지우는 audit 기록이 아니라 이전 audit 기록이므로, 감사 대상 table은 soft delete column으로만 지운다. session 변수, transaction setting, orm__context table은 쓰지 않는다. 정의는 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.4PostgreSQL 17SQLite
Tableinformation_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, attcollationpragma_table_xinfo와 AUTOINCREMENT, COLLATE, 제약 이름을 위한 sqlite_master.sql
Primary·unique key, indexinformation_schema.STATISTICS(INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLLATION, SUB_PART, EXPRESSION), TABLE_CONSTRAINTSpg_index(indkey, indoption, indpred), pg_get_indexdef, pg_constraintpragma_index_list, pragma_index_xinfo, sqlite_master.sql
Foreign keyREFERENTIAL_CONSTRAINTS, KEY_COLUMN_USAGEpg_constraint(contype='f', conkey, confkey, confupdtype, confdeltype, confmatchtype, condeferrable)pragma_foreign_key_list, 이름을 위한 sqlite_master.sql
CHECKCHECK_CONSTRAINTS.CHECK_CLAUSE, TABLE_CONSTRAINTS.ENFORCEDpg_constraint(contype='c'), pg_get_constraintdef, convalidatedsqlite_master.sql
Triggerinformation_schema.TRIGGERS(ACTION_TIMING, EVENT_MANIPULATION, ACTION_ORIENTATION, ACTION_STATEMENT)pg_trigger(tgtype, tgfoid), pg_get_triggerdef, pg_proc.prosrcsqlite_master(type='trigger')
CommentCOLUMNS.COLUMN_COMMENT, TABLES.TABLE_COMMENTpg_description(col_description, obj_description)없음
Sequence와 counterTABLES.AUTO_INCREMENTpg_sequences, pg_get_serial_sequencesqlite_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은 이름 순의 이름 있는 UNIQUE constraint로 쓴 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 순으로 쓴다.

dbspecMySQLPostgreSQLSQLite
i16, i32, i64SMALLINT, INT, BIGINTsmallint, integer, bigintsmallint, integer, bigint
booltinyint(1)booleanBOOLEAN
decimal(p,s)DECIMAL(p,s)numeric(p,s)DECIMALINT(p,s)
f64DOUBLEdouble precisionREAL
varchar(n)varchar(n) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_binvarchar(n) COLLATE "C"varchar(n)
textLONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bintext COLLATE "C"TEXT
bytesLONGBLOBbyteaBLOB
uuidchar(36) CHARACTER SET ascii COLLATE ascii_binuuidTEXT
dateDATEdateDATE
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는 MySQL CURRENT_TIMESTAMP(p), PostgreSQL statement_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에서는 now slot을 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의 모든 조건을 담는다.
dbspecMySQLPostgreSQLSQLite
i16, i32typeof(c) IN ('integer', 'null') AND c BETWEEN <min> AND <max>
i64(identity 아님)typeof(c) IN ('integer', 'null')
boolc 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
f64typeof(c) IN ('real', 'null')
varchar(n)length(c) <= n
uuidREGEXP_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개
datec 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 > 0time(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 > 0length(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 f64 CHECK는 위 schema definitions에 probe가 없다. REAL column은 정수와 숫자 text를 real 값으로 바꾸고 다른 text는 그대로 두는데, 이 CHECK가 그것을 거부한다(tests/dbspec/ddl.json의 types-and-defaults case). 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 column message를 일으킨다: MySQL SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '…', PostgreSQL RAISE EXCEPTION '…', SQLite SELECT RAISE(ABORT, '…').

  • DIA-130 immutable: 거부하는 BEFORE UPDATE와 BEFORE DELETE row trigger.

  • DIA-131 audit: AFTER INSERT row trigger는 이력 table에 action 'insert', NULL 이전 audit 기록, 기록하는 모든 column의 NEW 값을 넣는다. AFTER UPDATE row trigger는 'update', OLD.<audit column>, 기록하는 모든 column의 NEW 값을 넣는다. BEFORE DELETE row 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), main database(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 table dbspec$plans, plan이 숨긴 table과 column, SQLite 작업 table이다(plans).
  • DIA-136 database에 렌더링되는 setting만 읽는다: trigger로 알아보는 immutable과 audit. manifest setting은 문서에만 있다. 동기화하는 도구는 비교하는 문서에서 이를 유지한다.

Type과 column ​

CatalogMySQL COLUMN_TYPEPostgreSQL format_typeSQLite 선언 typedbspec
16, 32, 64-bit 정수smallint, int, bigintsmallint, integer, bigintrenderer CHECK가 있는 smallint, integer, biginti16, i32, i64
booleanrenderer CHECK가 있는 tinyint(1)booleanrenderer CHECK가 있는 BOOLEANbool
decimaldecimal(p,s)numeric(p,s)renderer CHECK가 있는 DECIMALINT(p,s)decimal(p,s)
실수doubledouble precisionrenderer CHECK가 있는 REALf64
길이 제한 textutf8mb4_0900_bin의 varchar(n)collation C의 character varying(n)renderer CHECK가 있는 varchar(n)varchar(n)
textutf8mb4_0900_bin의 longtextcollation C의 textCHECK 없는 TEXTtext
byteslongblobbyteaBLOBbytes
uuidrenderer CHECK가 있는 ascii_bin의 char(36)uuidrenderer CHECK가 있는 TEXTuuid
datedatedaterenderer CHECK가 있는 DATEdate
하루 중 시각renderer CHECK가 있는 time이나 time(p)renderer CHECK가 있는 time(p) without time zonep 하나의 renderer CHECK가 있는 TIMEtime(p)
date-timedatetime이나 datetime(p)timestamp(p) without time zonep 하나의 renderer CHECK가 있는 DATETIMEdatetime(p)
  • DIA-137 MySQL은 precision 0을 괄호 없이 쓴다(time, datetime). type, collation, character set이 이 표에 없는 column, renderer CHECK 없는 MySQL tinyint(1), char(36), time column, CHECK가 어떤 dbspec type의 renderer 출력도 아닌 SQLite column은 미지원으로 보고한다.

  • DIA-138 Null. IS_NULLABLE, attnotnull, pragma_table_xinfo.notnull이 null을 준다.

  • DIA-139 Identity. MySQL EXTRA auto_increment, PostgreSQL attidentity d, SQLite INTEGER PRIMARY KEY AUTOINCREMENT인 단일 column bigint primary key는 identity다. PostgreSQL attidentity a(ALWAYS)는 미지원이다.

  • DIA-140 Default. literal default는 column의 canonical default 형식으로 읽는다. MySQL COLUMN_DEFAULT는 escape를 푼 값(문자열, 숫자, bool이면 1이나 0)이다. PostgreSQL pg_get_expr는 '-5'::integer, 'a''b'::character varying, '2026-01-01'::date처럼 cast를 가진 literal을 주며 cast를 뗀다. SQLite dflt_value는 렌더링한 literal이며 decimal은 scale을 곱한 정수다. 시각 default는 renderer 출력과 같을 때 default now다: DEFAULT_GENERATED인 MySQL CURRENT_TIMESTAMP나 CURRENT_TIMESTAMP(p), PostgreSQL statement_timestamp(), 그 precision의 SQLite strftime 식. 그 밖의 식은 미지원이다.

Key, index, foreign key ​

  • DIA-141 primary key는 column 순서를 유지한다. MySQL UNIQUE index, PostgreSQL UNIQUE constraint, 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 TABLE text에서 온다.

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에 _ascii introducer를 주고 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 TABLE statement의 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', bool column의 1은 true가 된다. predicate로 읽히지 않거나 dbspec 규칙을 어기는 check는 미지원이다.

Trigger ​

  • DIA-150 각 setting은 제 trigger의 이름을 보고 알아보며, 그 이름은 dbspec 이름의 규칙으로 재계산한다. trigger 개수로 알아보지 않는다. <table>$immutable_update와 <table>$immutable_delete 이름의 trigger는 immutable setting을, <table>$audit_insert, <table>$audit_update, <table>$audit_delete 이름의 trigger는 audit setting을 준다. 각각 그 setting의 renderer 출력과 같을 때다: MySQL timing, event, ACTION_STATEMENT, PostgreSQL은 table의 schema prefix를 뗀 pg_get_triggerdef와 그 function의 prosrc, SQLite는 CREATE TRIGGER text. audit parameter는 비교 전에 audit_insert의 insert statement와 audit_update의 OLD column에서 읽는다. 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이나 audit setting마다 하나), view, routine, sequence, event, partition(MySQL이나 PostgreSQL의 partitioned table과 PostgreSQL partition), table(SQLite WITHOUT 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 TABLE text에서 읽는다. 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, avgmemory의 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_bycount와 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_duplicateinsert된 행과 만날 수 있는 keyinsert가 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_nowaitCAPABILITY_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요청마다 값 하나인 요청의 nowdatetime(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 columnsorm 요청에서는 client가 적용한다엔진이 시작하는 쓰기(CLI, MCP, sidecar, 이력)는 orm의 Go codec package로 codec을 적용하며, 검증은 같은 package로 decode한다. 저장 값은 encode된 값이며, byte를 만드는 단계는 base64다
indexes허용되며 메모리에서 파생된다
select explicit, entity, navigationclient 쪽이며 저장에 효과가 없다
aes, aes_version, blind_indexcorpus에서 schema 오류key는 client를 떠나지 않는다
audit, immutable settingcorpus에서 schema 오류대신 state_machine … history
transactionsession 위의 begin부터 commit/rollback까지readOnly는 지키며 lock을 잡는다
savepoint중첩 stage
orderByRandom지원순서에 seed를 쓰지 않는다
audit 값을 가진 transactionCAPABILITY_UNSUPPORTED
isolation, timeoutMs optionCAPABILITY_UNSUPPORTED
getQuery(), statement event, subscribe()CAPABILITY_UNSUPPORTEDSQL 텍스트가 없다
force_indexCAPABILITY_UNSUPPORTEDindex는 이름을 지정하지 않고 파생된다
fulltextOPERATOR_NOT_ALLOWEDSQLite는 table에 full-text index가 없다(orm dialects.md)
lock, local setting, privilege, AES rotation utilityCAPABILITY_UNSUPPORTED
schema install, dbplan chainCAPABILITY_UNSUPPORTEDflowmark 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_p i64 parameter를 사용하며 page 행에 적용하고 count는 여전히 모든 일치 행을 계산한다. 잘못된 parameter 값은 corpus를 읽기 전에 실패한다
  • DIA-159 insert(단일, 다중 행) — 결과: 지원; 규칙: identity, key_prefix, order column은 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_duplicate insert는 그 행 — 바이트 단위로 같은 key가 먼저, 아니면 byte 순서에서 가장 작은 것 — 과 만나고, 그 행은 자기 key를 유지한다. 근거: ON DUPLICATE KEY UPDATE와 ON CONFLICT는 충돌 key의 index가 그 index의 비교로 보고하는 중복에서 작동한다
  • DIA-165 lock update, share — 결과: 문서화된 no-op으로 받는다; 규칙: server transaction이 이미 corpus lock을 잡고 있다
  • DIA-166 lock update_nowait, share_nowait — 결과: CAPABILITY_UNSUPPORTED
  • DIA-167 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는 형태로 거절한다
  • DIA-168 subquery(tuple_in, tuple_not_in, scalar Sub aggregate) — 결과: 지원
  • DIA-169 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에서는 첫 행이므로, 한 행을 내는 형태만이 하나의 결과를 가진다
  • DIA-170 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에서 갈린다
  • DIA-171 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가 이 결과를 준다
  • DIA-172 contains_binary(Lb 연산자) — 결과: 지원; 규칙: column의 텍스트가 parameter의 텍스트를 substring으로 담으면 참이며, code point로 비교한다. NULL column이나 parameter는 unknown이며, parameter의 %, _, \는 문자 그대로다. 근거: utf8mb4_0900_bin 아래 MySQL LIKE, COLLATE "C" 아래 PostgreSQL LIKE, SQLite instr가 일치한다
  • 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, immutable setting — 결과: corpus에서 schema 오류; 규칙: 대신 state_machine … history
  • DIA-180 transaction — 결과: session 위의 begin부터 commit/rollback까지; 규칙: readOnly를 지킨다: 클라이언트는 read_only true인 begin을 쓰고, executor는 그 transaction에 쓰기 lock을 잡지 않으며 그 안의 모든 쓰기를 READ_ONLY로 거절한다
  • DIA-181 savepoint — 결과: 중첩 stage
  • DIA-182 orderByRandom — 결과: 지원; 규칙: 순서에 seed를 쓰지 않는다
  • DIA-183 audit 값을 가진 transaction — 결과: CAPABILITY_UNSUPPORTED
  • DIA-184 isolation, timeoutMs option — 결과: 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가 된다. 인용 문자열, 인용 식별자, 주석 안의 표지는 글자이므로 그대로 둔다. 드라이버가 문장 전체를 형식화하므로 문장의 모든 %는 %%로 쓴다. 값이 없는 문장은 인자 없이 그대로 드라이버에 넘긴다.

MIT License · Go / PHP / Rust / TypeScript