런타임 연결
클라이언트는 연결 값을 인자로 받으며 환경 변수나 비밀 저장소를 스스로 읽지 않는다. 클라이언트는 설정 파일을 읽지 않는다.
DSN URI만 데이터베이스를 선택한다.
mysql://user:password@host:3306/orm_example
mysql://user@localhost/orm_example?socket=/tmp/mysql.sock
postgres://user:password@host:5432/orm_example?sslmode=disable
postgres:///orm_example?host=/tmp
sqlite:///var/lib/orm_example.sqlite?_pragma=busy_timeout(5000)
markdown://127.0.0.1:4100/tracker?token=0123456789abcdef0123456789abcdef- CFG-1 scheme이 MySQL, PostgreSQL, SQLite 중 하나를 선택한다. 클라이언트는 해당 네이티브 드라이버를 만들고 연결 풀을 연다. 스키마 로딩과 해시 검사는 내부에서 처리한다. scheme
markdown은 데이터베이스 대신 flowmark server의 markdown IR executor를 선택한다. 그 DSN, server 연결, 거절하는 설정은 프로토콜과 capability 표에 있다. 모든 클라이언트가 이를 구현한다. Python 클라이언트는 이 연결에 server 연결 하나를 유지한다. - CFG-2 모든 연결은 서버 시간대와 상관없이 datetime 값을 UTC로 읽고 쓴다(dialects). MySQL 연결은
time_zone = '+00:00'을, PostgreSQL 연결은TimeZone을UTC로 설정하고, SQLite 값과 ORM 값 함수는 클라이언트에서 UTC를 쓴다. offset이 있는 값은 UTC wall clock으로 쓰고 UTC로 다시 읽는다. 선택 매개변수timezone은UTC나+00:00만 받으며, 다른 값은CONFIG를 반환한다. - CFG-3 모든 MySQL 연결은 열릴 때 드라이버 자체 설정 뒤에 세션
sql_mode를STRICT_ALL_TABLES,ONLY_FULL_GROUP_BY,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION로 정한다. 그래서 변환, 잘림, grouping 결과가 서버의sql_mode에 따르지 않고, 드라이버가 더한 mode(sqlx의PIPES_AS_CONCAT, client flag의IGNORE_SPACE)는 대체된다. 연결은@@SESSION.sql_mode를ONLY_FULL_GROUP_BY,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION로 읽는다(mysql.session.sql_mode_set,tests/dsn/mysql-session.json). - CFG-4 연결 설정은 최대 연결 수
poolSize를 받는다. Go는orm.Config{PoolSize: n}, PHP는new Config(poolSize: n), TypeScript는{ poolSize: n }, Rust는Db::connect(dsn, n, config), Python은connect(dsn, {"poolSize": n})이다. 0이거나 지정하지 않으면 풀을 유지하는 클라이언트는 최대 10개의 연결을 열고, 음수는CONFIG를 반환한다. 풀은 최대값보다 많은 연결을 열지 않으며, 모든 연결이 사용 중일 때 연결이 필요한 문이나 트랜잭션은 연결이 반환될 때까지 기다린다. TypeScript의 SQLite 드라이버는 연결 하나를 유지한다. PHP는 요청마다 연결을 연다. 각Db가 연결 하나를 열고 그 연결은Db와 함께 닫히며, 요청보다 오래 남는 연결은 없다. PHP의 풀은 PgBouncer나 ProxySQL이 맡는다(연결 pooler). PHP 클라이언트는 자기 풀을 가지지 않는다. php-fpm worker는 한 번에 요청 하나를 처리하고 다음 요청과 상태를 공유하지 않으며, 요청을 넘어 유지한 연결은 한 요청의 세션 상태를 다음 요청에 넘기고 서버 연결 수를 worker 수만큼 늘린다. 풀러는 대신 적은 서버 연결을 모든 worker가 나누어 쓰게 한다. 클라이언트가 무시하는 설정은 호출자가 연결이 풀링된다고 믿게 하므로, 0이 아닌 n의new Config(poolSize: n)은CONFIG를 반환한다.stats()는Db의 연결 하나인 1을maxOpenConnections로 보고한다. - CFG-5 연결 설정은 풀의 최대 유휴 연결 수
poolIdleSize와 풀 연결의 수명(밀리초)poolLifetimeMs를 받는다. Go는orm.Config{PoolIdleSize: n, PoolLifetimeMs: ms}, TypeScript는{ poolIdleSize: n, poolLifetimeMs: ms }, Rust는Config { pool_idle_size: n, pool_lifetime_ms: ms, ..Default::default() }, Python은connect(dsn, {"poolIdleSize": n, "poolLifetimeMs": ms})이며, PHP는 풀이 없어 이 항목의 끝처럼 이 값을 거부한다. 풀이 이미poolIdleSize개의 유휴 연결을 유지하는 동안 반환된 연결은 닫힌다. 수명이 지난 연결은 유휴 상태일 때 또는 반환될 때 닫힌다.poolIdleSize가 0이거나 지정하지 않으면 풀 크기까지 유휴 연결을 유지한다.poolLifetimeMs가 0이거나 지정하지 않으면 Rust를 뺀, 풀이 있는 모든 클라이언트는 수명 없이 연결을 유지하고, Rust는 풀의 30분 수명을 유지한다. 음수나 풀 크기보다 큰 유휴 연결 수는CONFIG를 반환하며, Rust의 필드는 부호 없는 정수다. TypeScript의 SQLite 드라이버는 연결 하나를 유지하며 이 값을 사용하지 않는다. PHP에는 풀이 없으므로 0이 아닌poolIdleSize나poolLifetimeMs에CONFIG를 반환한다. - CFG-6 연결 설정은 연결의 모든 문을 제한하는
statementTimeoutMs를 받는다. Go는orm.Config{StatementTimeoutMs: n}, PHP는new Config(statementTimeoutMs: n), TypeScript는{ statementTimeoutMs: n }, Rust는Config { statement_timeout_ms: n, ..Default::default() }, Python은connect(dsn, {"statementTimeoutMs": n})이다. 0이거나 지정하지 않으면 서버 기본값을 쓰고, 음수는CONFIG를 반환한다. PostgreSQL은statement_timeout으로 모든 문을 제한하며, 모든 클라이언트는 이 값을 각 서버 연결의 startup 매개변수로 전달한다. MySQL은max_execution_time으로 SELECT 문을 제한하며, 쓰기는 이 값이 아니라 서버 잠금 대기 시간(innodb_lock_wait_timeout)이 제한한다. SQLite에는 세션 시간 제한이 없으므로 DSN의busy_timeout이 그 한계다. 제한에 걸려 중단된 문은CANCELED를 반환한다. MySQL 잠금 대기 시간 초과(오류 1205)는 그 쓰기 한계가 끝난 것이므로 모든 클라이언트에서CANCELED를 반환한다. - CFG-7 PHP와 Python 클라이언트는 연결이 열릴 때 세션 설정을 정한다. PostgreSQL은 time zone과
statement_timeout을 startup 매개변수로 받고, MySQL은SET time_zone = '+00:00', sql_mode = '<declared modes>', SESSION max_execution_time = <ms>를 init command 하나로 실행한다.exit나 fatal error로 트랜잭션 안에서 끝난 PHP 요청은 그 연결도 함께 끝내며, 서버가 트랜잭션을 rollback하고 연결의 named lock과 세션 값을 푼다. - CFG-8 연결 설정은 현재 요청의 audit 값, 곧 계정이나 요청 id처럼 audit 기록 테이블의 컬럼 이름에서 값으로 가는 map을 돌려주는 함수
auditSource를 받는다. Go는orm.Config{AuditSource: func(ctx context.Context) (map[string]any, error)}(트랜잭션의 context로 부른다), PHP는new Config(auditSource: fn (): array => [...]), TypeScript는{ auditSource: () => ({...}) }, Rust는Config { audit_source: Some(Arc::new(|| Ok(vec![...]))), .. }, Python은connect(dsn, {"auditSource": lambda: {...}})다. audit 값을 가진 트랜잭션은 이 함수를 한 번 부르고 그 값과 자기 값으로 audit 기록을 삽입한다(사용법). ORM은 자기 audit 값을 쓰지 않으며, 함수의 오류는 트랜잭션을 실패시킨다. - CFG-9 흐름은 자기 언어의 취소 수단을 연결 핸들에 적용해 문을 취소한다. Go의
db.WithContext(ctx)와 TypeScript의db.withSignal(signal)은 같은 연결을 가리키는 핸들이다. 모델은 연결에 연결하듯 핸들에 연결하고, 핸들에서 시작한 트랜잭션도 같은 취소를 따르며, 컨텍스트나 시그널을 취소하면 실행 중인 문이CANCELED로 끝나고 연결은 계속 쓸 수 있다. Python의db.with_future(future)는 제공한asyncio.Future가 취소될 때 MySQL, PostgreSQL, SQLite 연결에서 실행 중인 문을 취소하는 핸들을 돌려준다. Python SQL은 동기 실행이므로 이벤트 루프가 취소를 처리하도록asyncio.to_thread에서 실행한다. transaction 정리는 취소하지 않으며 rollback 뒤 root 연결을 다시 쓸 수 있다. Python markdown executor는 문 취소를 지원하지 않는다. Rust는 문의 future를 버리면 문이 취소되고 풀로 돌아가기 전에 대여한 연결을 닫는다. PostgreSQL은 실행 중인 문을 취소하고 MySQL은 실행 중인 질의를 종료하며, TypeScript의 SQLite 드라이버는 문 하나를 중간에 양보하지 않고 실행하므로 시그널은 실행 중인 문이 아니라 다음 문에 적용된다. PHP$db->withCancellation($source)는 같은 논리 connection의 handle을 반환한다.$source->cancel()은 그 source에 연결된 handle에서 시작한 모든 진행 중 statement를CANCELED로 끝반환한다. SQLite는 progress handler로 virtual machine을 interrupt하고 MySQL은 control connection이 해당 source의 active server connection을 종료하며, transaction rollback 뒤 root handle은 다음 statement 전에 다시 연결한다. PostgreSQL에서는 PHP가pdo_pgsql과ext-pgsql을 모두 필요로 하며, 같은 물리 연결에서 protocol cancel request를 보내고 중단된 transaction을 rollback한 뒤 root 연결을 다시 사용한다. transaction 정리는 취소하지 않는다. PHP markdown executor는 문 취소를 지원하지 않는다. 체인에는 취소 연산이 없고 연결 핸들이 그 역할을 맡는다. 핸들은 별도의 값이므로 Go의db.Root(), Python의db.root(), TypeScript의db.root()는 파생의 기준이 된 연결을 돌려주며, 한 연결에서 파생한 핸들들은 같은 연결로 비교된다. - CFG-10 MySQL
socket매개변수는 Unix 소켓으로 연결한다. 클라이언트는 낙관적 갱신에 필요한 found-rows 개수를 켠다. - CFG-11 SQLite 경로는 절대 경로여야 한다. 클라이언트는 파일을 열기 전에 경로를 UTF-8로 percent-decode한다:
sqlite:///var/lib/my%20data.sqlite는/var/lib/my data.sqlite를 열고,%25,%23,%3F는 글자 그대로의%,#,?가 된다. 뒤에 hexadecimal 두 자리가 없는%, decode한 NUL byte, UTF-8이 아닌 decode 결과는CONFIG를 반환한다.tests/dsn/sqlite-paths.json이 공유 case를 담으며 모든 클라이언트가 자기 DSN 테스트에서 실행한다. 클라이언트는 외래 키를 켠다. 쓰기 트랜잭션은BEGIN IMMEDIATE로 시작해 시작 시점부터 데이터베이스의 쓰기 잠금을 가진다. 따라서 두 쓰기 트랜잭션은 차례로 실행되고, 읽은 뒤에 쓰는 트랜잭션도 실패하지 않는다.readOnly옵션을 쓴 트랜잭션은BEGIN으로 시작하고 쓰기 잠금을 얻지 않는다. 시작 문은 클라이언트가 정하며,_txlock이 있는 DSN은CONFIG를 반환한다. - CFG-12 SQLite 연결은 다른 연결이 가진 잠금을 최대 5000밀리초 기다린다. Go, PHP, PHP extension, TypeScript, Python, Rust 여섯 configured implementation은 DSN 매개변수
_pragma=busy_timeout(ms)로 다른 시간을 정한다. 예를 들어sqlite:///var/lib/orm_example.sqlite?_pragma=busy_timeout(10000)이며,busy_timeout(0)은 기다리지 않는다. 대기가 끝날 때까지 다른 연결이 잠금을 놓지 않으면CANCELED를 반환한다.DEADLOCK만 재시도하므로 이 트랜잭션은 다시 실행하지 않는다. 모든_pragma=name(value)매개변수는 연결에서PRAGMA name = value를 실행한다. - CFG-13 SQLite 읽기는 다른 연결이 쓰기 트랜잭션을 연 동안에도 실행되며, 그 전에 커밋된 행을 반환한다. 클라이언트는 데이터베이스 파일의 저널 모드를 그대로 두며 WAL을 요구하지 않는다. 기본 롤백 저널에서는 쓰기의 커밋 중에 시작한 읽기가
busy_timeout안에서 커밋을 기다린다._pragma=journal_mode(WAL)은 파일을 WAL로 바꾸며, WAL에서는 읽기가 커밋을 기다리지 않는다. 이 모드는 파일에 남는다. - CFG-14
@polyspec/orm의 SQLite 드라이버(TypeScript)는 잠금을 동기적으로 기다리므로 기다리는 동안 이벤트 루프가 멈춘다. 따라서 TypeScript 프로세스는 SQLite 파일 하나에Db하나를 연다. 그Db의 트랜잭션은 차례로 실행되고, 여러 프로세스는 잠금 대기로 파일을 함께 쓴다.
데이터베이스 자격 증명과 AES 키는 커밋하거나 런타임 파일에 기록하거나 로그에 남기지 않는다. 배포 비밀 저장소에서 값을 읽은 뒤 클라이언트 연결 옵션으로 전달한다.
연결 끊김
관리자가 세션을 끝낼 때(pg_terminate_backend, KILL), 서버가 다시 시작할 때, 연결이 한도보다 오래 유휴일 때(PostgreSQL idle_session_timeout, MySQL wait_timeout) 서버는 연결을 끝내고, 네트워크 장애는 연결을 끊는다. 그 연결의 다음 문은 실패하며, 모든 클라이언트는 드라이버와 그 메시지와 상관없이 그 실패를 CONNECTION_LOST 코드로 보고한다. MySQL 오류 2006, 2013, 4031, PostgreSQL SQLSTATE class 08과 57P01, 57P02, 57P05, 그리고 더 이상 열려 있지 않은 연결에 대한 드라이버의 오류다(PHP pdo_pgsql은 backend process id가 0인 연결에서 서버 SQLSTATE 없이 보고한다. go-sql-driver ErrInvalidConn, driver.ErrBadConn, Rust의 끊긴 socket, mysql2의 fatal 오류, 연결 오류를 알린 node-postgres 연결, 닫힌 연결의 PyMySQL InterfaceError, psycopg가 닫힌 것으로 표시한 연결에서 서버 SQLSTATE 없이 온 psycopg 오류). SQLite에는 서버 연결이 없다.
- CFG-15 모든 클라이언트는 그렇게 실패한 문에
CONNECTION_LOST를 반환하고 그 문을 다시 전송하지 않는다. 연결의 트랜잭션이나 세션 상태가 사라졌기 때문이다.
DSN 매개변수와 MySQL TLS
MySQL DSN은 모든 클라이언트가 구현하는 한 가지 모드로 TLS 연결한다.
mysql://user:password@db.example:3306/orm_example?ssl-mode=VERIFY_IDENTITY&ssl-ca=/etc/orm/mysql-ca.pem- CFG-16
ssl-mode=VERIFY_IDENTITY와 인증 기관의 PEM file 절대 경로인ssl-ca는 server 인증서를 그 인증 기관으로, DSN의 host를 인증서로 검사하는 TLS로 연결한다. 다른 인증 기관이 서명했거나 다른 host를 이름한 인증서는 연결을 실패시킨다. 그러면 password는 TLS 안에서만 전달되며, 클라이언트는 server의 RSA 공개 키를 요청하지 않는다. - CFG-17 host는 server 인증서가 이름하는 host 이름이며 IP 주소가 아니다. TypeScript driver가 주소 host를 이름
localhost로 검사하기 때문이며, 모든 클라이언트는ssl-mode와 함께 쓴 주소에CONFIG를 반환한다. 이 모드는 TCP로 연결하며socket을 받지 않는다. - CFG-18
VERIFY_CA나REQUIRED같은ssl-mode의 다른 값,ssl-ca없는ssl-mode,ssl-mode없는ssl-ca, 상대ssl-ca경로, 읽을 수 없는 CA file은 Rust를 뺀 모든 클라이언트에서CONFIG를 반환한다. Rust의 매개변수는 아래에 있다. - CFG-19 Go와 Rust를 뺀 모든 클라이언트는 다음 매개변수만 받고 다른 매개변수에는
CONFIG를 반환한다. MySQL은timezone,socket,ssl-mode,ssl-ca, PostgreSQL은timezone,host,sslmode, SQLite는timezone,_pragma, 그리고 그 자체로CONFIG를 반환하는_txlock이다. - CFG-20 클라이언트는 받는 다른 매개변수가 서로 다르다. Go 클라이언트는 다른 MySQL 매개변수를 go-sql-driver/mysql에 전달하며, 이 driver는 그 이름의 session 변수나 driver option을 설정한다. 다른 PostgreSQL 매개변수는 pgx에 전달한다. Rust 클라이언트는
timezone을 뺀 모든 매개변수를 sqlx에 전달하며, sqlx는 대소문자와 관계없이verify_ca같은 다른ssl-mode값도 받는다. 이 page의 매개변수만 이름한 DSN은 모든 클라이언트에서 같게 동작한다. Go와 Rust 클라이언트가 다른 매개변수를 거부하게 하면 현재 사용자의 MySQL DSN이 바뀌며, 그 사용자는 Rust 클라이언트로ssl-mode=verify_ca를 쓰므로 이는 별도의 변경이다.
Primary와 replica
ORM은 문을 서버 사이에서 분배하지 않는다. replica에서 읽을 때는 서버마다 연결을 하나씩 쓴다. primary에 하나, replica마다 하나를 열고, 문이 쓸 연결을 connect로 모델이나 행에 전달한다.
| 언어 | 연결 | replica로 읽기 | primary로 쓰기 |
|---|---|---|---|
| PHP | $master = \Polyspec\Orm\Tests\Model\connect($primaryDsn, $config); $slave1 = \Polyspec\Orm\Tests\Model\connect($replicaDsn, $config); | (new User)($slave1)->name($name)->get() | $row->connect($master)->setName($new)->update() |
| Go | master, err := model.Connect(primaryDSN, cfg) slave1, err := model.Connect(replicaDSN, cfg) | model.User().Connect(slave1).Name(name).Get() | row.Connect(master).SetName(next).Update() |
| Rust | let master = model::connect(&primary_dsn, n, cfg.clone()).await?; let slave1 = model::connect(&replica_dsn, n, cfg).await?; | User::new().connect(&slave1).name(name).get().await? | row.connect(&master).set_name(next).update(false).await? |
| TypeScript | const master = await connect(primaryDsn, options); const slave1 = await connect(replicaDsn, options); | new User().connect(slave1).name(name).get() | row.connect(master).setName(next).update() |
| Python | master = model.connect(primary_dsn, **options) slave1 = model.connect(replica_dsn, **options) | model.User().connect(slave1).name(name).get() | row.connect(master).set_name(new).update() |
- CFG-21 모델이나 읽어 온 행은 자신이 연결된 연결만 사용한다.
master의 트랜잭션 안에서slave1에 연결한 모델은 트랜잭션 밖에서slave1로 실행되고,connect가 없는 모델은 트랜잭션 안에서 실행된다. - CFG-22 연결마다 풀, prepared statement, 세션 설정이 따로 있다.
- CFG-23 replica는 primary가 commit한 변경을 그 뒤에 적용한다. 같은 요청의 쓰기를 읽어야 하는 읽기는 primary 연결을 사용하며, orm은 연결을 스스로 고르지 않는다.
- CFG-24 replica는 쓰기를 거부한다. PostgreSQL은 SQLSTATE 25006을,
super_read_only인 MySQL replica는 오류 1290을 반환한다. 클라이언트는 읽기 전용 트랜잭션의 쓰기와 읽기 전용으로 열린 SQLite 데이터베이스에 대한 쓰기와 마찬가지로 드라이버 메시지와 함께READ_ONLY를 반환한다. - CFG-25 스키마 도구와
utils().schema().install은 primary에서 실행하고, replica는 복제로 스키마를 적용한다. - CFG-26 SQLite는 단일 노드 전용이다. SQLite 데이터베이스는 절대 경로로 지정한 로컬 파일이며, ORM은 SQLite replica를 지원하지 않는다.
make orm-test-servers는 MySQL primary의 replica와 PostgreSQL primary의 standby를 시작한다. 이 규칙은 Go, PHP, PHP extension, TypeScript, Python, Rust 여섯 configured implementation에 적용되며 PHP extension의 전체 database verification은 아직 남아 있다. PostgreSQL standby는 physical replication slot orm_replica로 stream하므로, standby가 멈춘 동안에도 primary는 standby가 아직 받지 않은 WAL을 보존한다.
연결 pooler
연결 pooler는 클라이언트와 데이터베이스 서버 하나 사이에서 적은 수의 서버 연결을 많은 클라이언트 연결이 나누어 쓰게 한다. Go, PHP, PHP extension, TypeScript, Python, Rust 여섯 configured implementation에 공통 pooler 규칙이 적용된다. 클라이언트는 PostgreSQL에서 transaction 모드의 PgBouncer, MySQL에서 ProxySQL을 통해 동작하며, 둘은 pooler로만 쓴다. 클라이언트는 해당 서버의 DSN으로 pooler에 연결하고, pooler는 어떤 문도 다른 서버로 보내지 않는다. make client-pooler-check는 선언된 database case를 실행하며, PHP extension database coverage가 완료됐음을 뜻하지 않는다.
PgBouncer 설정:
CFG-27
pool_mode:transaction. 서버 연결 하나가 한 번에 클라이언트 트랜잭션 하나 또는 트랜잭션 밖의 문 하나를 처리한다.CFG-28
max_prepared_statements: 0보다 큰 값(PgBouncer 기본값 200). Go 클라이언트, Rust 클라이언트, TypeScript 클라이언트는 문을 프로토콜 수준의 이름 있는 prepared statement로 실행하고, PHP 클라이언트는 PostgreSQL 문을 bind와 함께 unnamed statement로 실행하며, Python 클라이언트(psycopg)는 문을 unnamed로 실행하다가 그 연결에서 다섯 번 실행한 뒤 이름 있는 statement로 준비한다. 0이면 한 서버 연결에서 준비한 문이 다음 서버 연결에 없어서 PgBouncer가prepared statement … already exists또는does not exist를 반환한다.CFG-29
track_extra_parameters:statement_timeout. 연결이statementTimeoutMs를 설정할 때 필요하다. 클라이언트는statement_timeout을 startup 매개변수로 전달한다. PgBouncer는 추적하지 않는 startup 매개변수를 거부하고, 추적하는 매개변수는 그 클라이언트 연결에 배정하는 모든 서버 연결에만 설정한다.CFG-30 클라이언트가 트랜잭션 밖에서 정하는 PostgreSQL 세션 상태는 시간대뿐이다. 모든 클라이언트는 시간대를 startup 매개변수
TimeZone=UTC로 보내며, PgBouncer는TimeZone을 기본으로 추적한다. 트랜잭션 옵션(SET TRANSACTION,SET LOCAL statement_timeout),utils().setLocal(set_config(…, true)),utils().lock(pg_advisory_xact_lock)은 트랜잭션과 함께 끝난다.CFG-31 문 취소는 PgBouncer를 거쳐서도 동작한다. PgBouncer는 클라이언트 연결의 프로토콜 취소 요청을 그 문을 실행하는 서버 연결로 전달한다. TypeScript는 연결의 process id와 secret key로 그 요청을 전송하고, PHP는 PostgreSQL 연결에서
ext-pgsql로 protocol cancel request를 전송한다.CFG-32
polyspec-ormcrate, 곧 Rust 클라이언트는 startup 매개변수extra_float_digits를 보내지 않는다. PostgreSQL 12 이상은 서버 기본값으로 float8 값을 정확히 출력한다.CFG-33 위반된 foreign key, check 또는 unique key는
FOREIGN_KEY,CONSTRAINT또는DUPLICATE_KEY로 보고되며, 각 database의 native code는 errors.yaml이 나열한 것이고, statement는 어떤 row도 바꾸지 않는다. 이는 CFG-6의1205규칙 옆의 대응이다.
ProxySQL은 기본 multiplexing으로 실행하며 ORM 전용 설정이 필요 없다. ProxySQL은 클라이언트 연결마다 time_zone, sql_mode, max_execution_time을 기억하고 그 연결에 쓰는 모든 서버 연결에 적용한다. 사용자 변수, GET_LOCK, CREATE TEMPORARY TABLE을 포함한 문이 실행되면 ProxySQL은 그 클라이언트 연결이 끊어질 때까지 서버 연결 하나에 고정한다. MySQL에서 utils().setLocal은 사용자 변수 @`orm.<key>`를, utils().lock은 GET_LOCK을 쓰므로 이들을 사용한 연결은 더 이상 multiplexing되지 않는다. 결과는 달라지지 않는다.
migration plan 적용은 pooler를 거치지 않고 primary에 직접 연결한다. 적용이 세션 범위의 MySQL GET_LOCK이나 PostgreSQL advisory lock을 잡기 때문이다(plans).