├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── one-ck.yml ├── composer.json ├── src ├── CkException.php ├── Protocol.php ├── Read.php ├── Write.php ├── Client.php └── Types.php ├── README.md ├── test.php └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /App/RunCache 3 | .idea 4 | .vscode 5 | .svn 6 | .DS_Store 7 | composer.lock 8 | bh.php -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # 如果对你有帮助 欢迎打赏 2 | 3 | custom: ["https://www.vicsdf.com/img/z.jpg","https://www.vicsdf.com/img/w.jpg"] 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lizhichao/one-ck", 3 | "type": "library", 4 | "description": "clickhouse tcp client", 5 | "keywords": [ 6 | "php", 7 | "tcp", 8 | "client", 9 | "clickhouse" 10 | ], 11 | "license": "Apache-2.0", 12 | "authors": [ 13 | { 14 | "name": "tanszhe", 15 | "email": "1018595261@qq.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.2", 20 | "ext-json": "*", 21 | "ext-bcmath":"*" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "OneCk\\": "src/" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/CkException.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 15 | } 16 | 17 | public function getChar($n = 1) 18 | { 19 | $buffer = fread($this->conn, $n); 20 | if ($buffer === false || !isset($buffer[0])) { 21 | throw new CkException('read from fail', CkException::CODE_READ_FAIL); 22 | } 23 | if (strlen($buffer) < $n) { 24 | $buffer .= $this->getChar($n - strlen($buffer)); 25 | } 26 | return $buffer; 27 | } 28 | 29 | /** 30 | * @return int 31 | * @throws CkException 32 | */ 33 | public function number() 34 | { 35 | $r = 0; 36 | $b = 0; 37 | while (1) { 38 | $j = ord($this->getChar()); 39 | $r = (($j & 127) << ($b * 7)) | $r; 40 | if ($j < 128) { 41 | return $r; 42 | } 43 | $b++; 44 | } 45 | } 46 | 47 | public function echo_str($n = 50) 48 | { 49 | $s = $this->getChar($n); 50 | echo "--- start ---\n"; 51 | echo 'total len ' . strlen($s) . PHP_EOL; 52 | echo $s . PHP_EOL; 53 | for ($i = 0; $i < strlen($s); $i++) { 54 | echo $i . '=> ' . $s[$i] . '=>' . ord($s[$i]) . PHP_EOL; 55 | } 56 | echo PHP_EOL; 57 | echo "--- end ---\n"; 58 | } 59 | 60 | /** 61 | * @return int 62 | * @throws CkException 63 | */ 64 | public function int() 65 | { 66 | return unpack('l', $this->getChar(4))[1]; 67 | } 68 | 69 | /** 70 | * @return string 71 | * @throws CkException 72 | */ 73 | public function string() 74 | { 75 | $n = $this->number(); 76 | return $n === 0 ? '' : $this->getChar($n); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Write.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 17 | } 18 | 19 | 20 | /** 21 | * @param int ...$nr 22 | * @return $this 23 | */ 24 | public function number(...$nr) 25 | { 26 | $r = []; 27 | foreach ($nr as $n) { 28 | $b = 0; 29 | while ($n >= 128) { 30 | $r[] = $n | 128; 31 | $b++; 32 | $n = $n >> 7; 33 | } 34 | $r[] = $n; 35 | } 36 | if ($r) { 37 | $this->buf .= pack('C*', ...$r); 38 | } 39 | return $this; 40 | } 41 | 42 | /** 43 | * @param int $n 44 | * @return $this 45 | */ 46 | public function int($n) 47 | { 48 | $this->buf .= pack('l', $n); 49 | return $this; 50 | } 51 | 52 | /** 53 | * @param int ...$str 54 | * @return $this 55 | */ 56 | public function string(...$str) 57 | { 58 | foreach ($str as $s) { 59 | $this->number(strlen($s)); 60 | $this->buf .= $s; 61 | } 62 | return $this; 63 | } 64 | 65 | 66 | /** 67 | * @param $str 68 | * @return $this 69 | */ 70 | public function addBuf($str) 71 | { 72 | $this->buf .= $str; 73 | return $this; 74 | } 75 | 76 | 77 | public function flush() 78 | { 79 | if ($this->buf === '') { 80 | return true; 81 | } 82 | $len = fwrite($this->conn, $this->buf); 83 | if ($len !== strlen($this->buf)) { 84 | throw new CkException('write fail', CkException::CODE_WRITE_FAIL); 85 | } 86 | $this->buf = ''; 87 | return true; 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 一个php实现的clickhouse tcp协议客户端 | php tcp client for clickhouse 2 | 3 | - 支持流式写入 Support streaming writing 4 | - 超高的性能,支持1000w/s的写入速度 Super high performance, Support 1000w/s writing speed 5 | - tcp对服务器对服务器友好,压力小 tcp is server-friendly, with little pressure 6 | - 零依赖 Zero dependence 7 | 8 | ### 安装 | install 9 | 10 | `composer require lizhichao/one-ck` 11 | 12 | > `php5.6 - php7.1` 不支持 `Decimal128` ,请使用 `0.1.x` 版本 13 | > `php7.2 >= ` 推荐使用 `0.2.x`版本 14 | > clickhouse QQ交流群 970435787 15 | 16 | 17 | ### 支持的数据类型 | Supported types 18 | - [x] UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64 19 | - [x] Float32, Float64 20 | - [x] Decimal(P, S), Decimal32(S), Decimal64(S), Decimal128(S) 21 | - [x] String 22 | - [x] Nothing 23 | - [x] FixedString(N) 24 | - [x] UUID 25 | - [x] Date 26 | - [x] Datetime 27 | - [x] Datetime64 28 | - [x] Nullable(T) 29 | - [x] IPv4 30 | - [x] IPv6 31 | - [x] Array(T) 32 | - [x] SimpleAggregateFunction(F, T) 33 | - [ ] Tuple(T) 34 | - [ ] Int128,Int256,UInt128,UInt256,Decimal256(S) 35 | - [ ] Bool 36 | - [ ] JSON 37 | - [ ] Enum (can use `LowCardinality` instead) 38 | 39 | ### Array of client options 40 | 41 | It is also possible to create the client with configuration options: 42 | 43 | ```php 44 | use OneCk\Client; 45 | 46 | $client = new Client( 47 | 'tcp://localhost:9000', 48 | 'username', 49 | 'password', 50 | 'database', 51 | [ 52 | 'connect_timeout' => 3, 53 | 'socket_timeout' => 30, 54 | 'tcp_nodelay' => true, 55 | 'persistent' => true, 56 | ] 57 | ); 58 | ``` 59 | 60 | The following options are available: 61 | 62 | Name | Type | Default | Description 63 | --- | :---: | :---: | --- 64 | *connect_timeout* | float | 3.0 | The number of seconds that the client waits for a connect to a ClickHouse server before throwing a `CkException` exception. 65 | *socket_timeout* | float | 30.0 | The number of seconds that the client waits for a respond from a ClickHouse server before throwing a `CkException` exception. 66 | *tcp_nodelay* | boolean | false | Whether the Nagle algorithm is disabled on a TCP connection. 67 | *persistent* | boolean | false | Whether to use a persistent connection. 68 | 69 | ### 使用例子 | Demo 70 | ```php 71 | use OneCk\Client; 72 | use OneCk\Types; 73 | //default 74 | //$ck = new Client('tcp://127.0.0.1:9000'); 75 | 76 | $t1 = microtime(true); 77 | $ck = new Client('tcp://192.168.31.216:9091', 'default', '123456', 'test1'); 78 | 79 | $data['server info'] = $ck->getServerInfo(); 80 | $data['drop table'] = $ck->query('DROP TABLE IF EXISTS t6'); 81 | $table = [ 82 | 'CREATE TABLE t6 (', 83 | '`id` UInt32,', 84 | '`f1` Int32,', 85 | '`f2` Nullable(Int32),', 86 | '`f3` UInt8,', 87 | '`f4` Nullable(UInt8),', 88 | '`f5` UInt16,', 89 | '`f6` UInt64,', 90 | '`f7` Int64,', 91 | '`f8` Float32,', 92 | '`f9` Float64,', 93 | '`f10` Nullable(Float64),', 94 | '`f11` Decimal32(3),', 95 | '`f12` Decimal64(5),', 96 | '`f13` Decimal128(7),', 97 | '`f14` Nullable(Decimal128(7)),', 98 | '`f15` String,', 99 | '`f16` Nullable(String),', 100 | '`f17` FixedString(32),', 101 | '`f18` UUID,', 102 | '`f19` Date,', 103 | '`f20` Nullable(Date),', 104 | '`f21` Datetime,', 105 | '`f22` Datetime64(3),', 106 | '`f23` IPv4,', 107 | '`f24` Nullable(IPv4),', 108 | '`f25` IPv6,', 109 | '`f26` LowCardinality(String),', 110 | '`f27` Array(Int32),', 111 | '`f28` Array(Array(Array(Nullable(Date)))),', 112 | '`f29` Array(Array(Array(Array(Array(Nullable(Datetime))))))', 113 | ') ENGINE = MergeTree() ORDER BY id SETTINGS index_granularity = 8192' 114 | ]; 115 | $data['create table'] = $ck->query(implode("\n", $table)); 116 | 117 | $data['insert data'] = $ck->insert('t6', [ 118 | [ 119 | 'id' => 1, 120 | 'f1' => -3, 121 | 'f2' => null, 122 | 'f3' => 127, 123 | 'f4' => null, 124 | 'f5' => 3322, 125 | 'f6' => 1844674407370955161, 126 | 'f7' => 9223372036854775807, 127 | 'f8' => -2132121.5, 128 | 'f9' => 6546546544665.66658, 129 | 'f10' => null, 130 | 'f11' => 552.339, 131 | 'f12' => 3658.6954, 132 | 'f13' => '170141183460469231168730371588.4105721', 133 | 'f14' => null, 134 | 'f15' => 'emoji😀😁😂😃😄', 135 | 'f16' => null, 136 | 'f17' => md5('a'), 137 | 'f18' => '016e64be-605f-4108-8a67-495d74d7ef3c', 138 | 'f19' => '2020-09-05', 139 | 'f20' => null, 140 | 'f21' => '2020-09-05 14:25:12', 141 | 'f22' => '2020-09-05 14:25:12.258', 142 | 'f23' => '192.168.1.1', 143 | 'f24' => null, 144 | 'f25' => 'CDCD:910A:2222:5498:8475:1111:3900:2020', 145 | 'f26' => 'eee', 146 | 'f27' => [0, -2, 3, 4, 5, 6, 7, 8, 64], 147 | 'f28' => [[['2020-01-05', null, '2020-01-06']], [['2020-01-07'], ['2020-01-08']], [['2020-01-09']]], 148 | 'f29' => [[[[["2020-01-05 05:05:05", null, "2020-01-06 15:16:17"]], [["2020-01-07 18:19:20"], ["2020-01-08 21:22:23"]], [["2020-01-09 00:00:00"]]], [[["2020-01-10 01:00:00", null]]]], [[[["2020-01-11 00:00:01", null, "2020-01-12 11:01:58"]], [["2020-01-13 21:22:01"]]]]] 149 | ], 150 | [ 151 | 'id' => 2, 152 | 'f1' => 3, 153 | 'f2' => 3, 154 | 'f3' => 3, 155 | 'f4' => 3, 156 | 'f5' => 3, 157 | 'f6' => 3, 158 | 'f7' => 3, 159 | 'f8' => 3, 160 | 'f9' => 3, 161 | 'f10' => 3, 162 | 'f11' => -552.339, 163 | 'f12' => -3658.6954, 164 | 'f13' => '-170141183460469231168730371588.4105721', 165 | 'f14' => 3, 166 | 'f15' => str_repeat(md5('aa'), '6'), 167 | 'f16' => '', 168 | 'f17' => md5('55'), 169 | 'f18' => md5('55'), 170 | 'f19' => '2020-09-06', 171 | 'f20' => '2020-09-06', 172 | 'f21' => '2020-09-06 14:25:12', 173 | 'f22' => '2020-09-06 14:25:12.258', 174 | 'f23' => '251.222.221.231', 175 | 'f24' => '192.168.1.2', 176 | 'f25' => '1030::C9B4:FF12:48AA:1A2B', 177 | 'f26' => 'eee22', 178 | 'f27' => [1, 2, 3, 4], 179 | 'f28' => [[['2020-01-05', '2020-01-06']], [['2020-01-07', null], ['2020-01-08']], [['2020-01-09']]], 180 | 'f29' => [[[[[null]]]]] 181 | ], 182 | [ 183 | 'id' => 3, 184 | 'f1' => -1, 185 | 'f2' => 3, 186 | 'f3' => 3, 187 | 'f4' => 3, 188 | 'f5' => 3, 189 | 'f6' => 3, 190 | 'f7' => 3, 191 | 'f8' => 3, 192 | 'f9' => 3, 193 | 'f10' => 3, 194 | 'f11' => 3, 195 | 'f12' => 3, 196 | 'f13' => 3, 197 | 'f14' => 3, 198 | 'f15' => 'aaa', 199 | 'f16' => 'aaa', 200 | 'f17' => md5('a'), 201 | 'f18' => '3026ee79-ac2a-46d2-882d-959a55d71025', 202 | 'f19' => '2020-09-07', 203 | 'f20' => '2020-09-07', 204 | 'f21' => '2020-09-07 14:25:12', 205 | 'f22' => '2020-09-07 14:25:12.258', 206 | 'f23' => '192.168.1.1', 207 | 'f24' => null, 208 | 'f25' => '2001:DB8:2de::e13', 209 | 'f26' => 'eee22', 210 | 'f27' => [12344], 211 | 'f28' => [[['2020-01-05', '2020-01-06'], [null]], [['2020-01-07'], ['2020-01-08']], [['2020-01-09']]], 212 | 'f29' => [[[[['2018-01-25 11:25:14']]]]] 213 | ] 214 | ]); 215 | 216 | 217 | //$data['struct'] = $ck->query('desc t6'); 218 | 219 | 220 | $data['select t6'] = $ck->query('select * from t6'); 221 | 222 | $data['select t6 int64'] = $ck->query("select id,f6 from t6 where f6=1844674407370955161"); 223 | 224 | $data['select t6 Decimal32'] = $ck->query("select id,f11 from t6 where f11='552.339'"); 225 | 226 | $data['select t6 Decimal64'] = $ck->query("select id,f12 from t6 where f12='-3658.69540'"); 227 | 228 | $data['select t6 Decimal128'] = $ck->query("select id,f13 from t6 where f13='170141183460469231168730371588.4105721'"); 229 | 230 | $data['select t6 uuid'] = $ck->query("select id,f18 from t6 where f18='3026ee79-ac2a-46d2-882d-959a55d71025'"); 231 | 232 | $data['select t6 date'] = $ck->query("select id,f19 from t6 where f19='2020-09-05'"); 233 | 234 | $data['select t6 datetime'] = $ck->query("select id,f21 from t6 where f21='2020-09-07 20:25:12'"); 235 | 236 | $data['select t6 datetime64'] = $ck->query("select id,f22 from t6 where f22='2020-09-06 20:25:12.258'"); 237 | 238 | $data['select t6 ip'] = $ck->query("select id,f23,f25 from t6 where f23=" . Types::encodeIpv4('192.168.1.1')); 239 | 240 | $data['select t6 ip64'] = $ck->query("select id,f23,f25 from t6 where f25='" . Types::encodeIpv6('1030::c9b4:ff12:48aa:1a2b') . "'"); 241 | 242 | $data['nothing'] = $ck->query('select array()'); 243 | 244 | 245 | 246 | // flow of write 247 | $data['drop table'] = $ck->query('DROP TABLE IF EXISTS t7'); 248 | $table = [ 249 | 'CREATE TABLE t7 (', 250 | '`id` UInt32,', 251 | '`f2` Nullable(Int32),', 252 | '`f5` UInt16,', 253 | '`f15` String', 254 | ') ENGINE = MergeTree() ORDER BY id SETTINGS index_granularity = 8192' 255 | ]; 256 | $data['create table'] = $ck->query(implode("\n", $table)); 257 | $ck->writeStart('t7',['id','f2','f5','f15']); 258 | for ($i = 0; $i < 100; $i++) { 259 | $da = []; 260 | for ($j = 0; $j < 1000; $j++) { 261 | $da[] = [ 262 | 'id' => mt_rand(1,1000000), 263 | 'f2' => mt_rand(-1000000,1000000), 264 | 'f5' => mt_rand(1,10000), 265 | 'f15' => md5(mt_rand(1,10000)) 266 | ]; 267 | } 268 | $ck->writeBlock($da); 269 | } 270 | 271 | $ck->writeEnd(); 272 | 273 | $data['write 10w rows time'] = microtime(true) - $t1; 274 | 275 | echo json_encode($data); 276 | 277 | //Close connection 278 | unset($ck); 279 | ``` 280 | 281 | ## 我的其他仓库 282 | 283 | * [一个极简高性能php框架,支持[swoole | php-fpm ]环境](https://github.com/lizhichao/one) 284 | * [国密sm3 sm4库](https://github.com/lizhichao/sm) 285 | * [中文分词](https://github.com/lizhichao/VicWord) 286 | * [nsq客户端](https://github.com/lizhichao/one-nsq) 287 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | getServerInfo(); 14 | $data['drop table'] = $ck->query('DROP TABLE IF EXISTS t6'); 15 | 16 | $res = $ck->query("SELECT sipHash64(toString('1ace54')) AS result"); 17 | if($res[0]['result'] !== '9525649478782099197'){ 18 | throw new \Exception('test uint64 fail:' . __LINE__); 19 | } 20 | $table = [ 21 | 'CREATE TABLE t6 (', 22 | '`id` UInt32,', 23 | '`f1` Int32,', 24 | '`f2` Nullable(Int32),', 25 | '`f3` UInt8,', 26 | '`f4` Nullable(UInt8),', 27 | '`f5` UInt16,', 28 | '`f6` UInt64,', 29 | '`f7` Int64,', 30 | '`f8` Float32,', 31 | '`f9` Float64,', 32 | '`f10` Nullable(Float64),', 33 | '`f11` Decimal32(3),', 34 | '`f12` Decimal64(5),', 35 | '`f13` Decimal128(7),', 36 | '`f14` Nullable(Decimal128(7)),', 37 | '`f15` String,', 38 | '`f16` Nullable(String),', 39 | '`f17` FixedString(32),', 40 | '`f18` UUID,', 41 | '`f19` Date,', 42 | '`f20` Nullable(Date),', 43 | '`f21` Datetime,', 44 | '`f22` Datetime64(3),', 45 | '`f23` IPv4,', 46 | '`f24` Nullable(IPv4),', 47 | '`f25` IPv6,', 48 | '`f26` LowCardinality(String),', 49 | '`f27` Array(Int32),', 50 | '`f28` Array(Array(Array(Nullable(Date)))),', 51 | '`f29` Array(Array(Array(Array(Array(Nullable(Datetime))))))', 52 | ') ENGINE = MergeTree() ORDER BY id SETTINGS index_granularity = 8192' 53 | ]; 54 | $data['create table'] = $ck->query(implode("\n", $table)); 55 | 56 | $data['insert data'] = $ck->insert('t6', [ 57 | [ 58 | 'id' => 1, 59 | 'f1' => -3, 60 | 'f2' => null, 61 | 'f3' => 127, 62 | 'f4' => null, 63 | 'f5' => 3322, 64 | 'f6' => 1844674407370955161, 65 | 'f7' => 9223372036854775807, 66 | 'f8' => -2132121.5, 67 | 'f9' => 6546546544665.66658, 68 | 'f10' => null, 69 | 'f11' => 552.339, 70 | 'f12' => 3658.6954, 71 | 'f13' => '170141183460469231168730371588.4105721', 72 | 'f14' => null, 73 | 'f15' => 'emoji😀😁😂😃😄', 74 | 'f16' => null, 75 | 'f17' => md5('a'), 76 | 'f18' => '016e64be-605f-4108-8a67-495d74d7ef3c', 77 | 'f19' => '2020-09-05', 78 | 'f20' => null, 79 | 'f21' => '2020-09-05 14:25:12', 80 | 'f22' => '2020-09-05 14:25:12.258', 81 | 'f23' => '192.168.1.1', 82 | 'f24' => null, 83 | 'f25' => 'CDCD:910A:2222:5498:8475:1111:3900:2020', 84 | 'f26' => 'eee', 85 | 'f27' => [0, -2, 3, 4, 5, 6, 7, 8, 64], 86 | 'f28' => [[['2020-01-05', null, '2020-01-06']], [['2020-01-07'], ['2020-01-08']], [['2020-01-09']]], 87 | 'f29' => [[[[["2020-01-05 05:05:05", null, "2020-01-06 15:16:17"]], [["2020-01-07 18:19:20"], ["2020-01-08 21:22:23"]], [["2020-01-09 00:00:00"]]], [[["2020-01-10 01:00:00", null]]]], [[[["2020-01-11 00:00:01", null, "2020-01-12 11:01:58"]], [["2020-01-13 21:22:01"]]]]] 88 | ], 89 | [ 90 | 'id' => 2, 91 | 'f1' => 3, 92 | 'f2' => 3, 93 | 'f3' => 3, 94 | 'f4' => 3, 95 | 'f5' => 3, 96 | 'f6' => '9844674407370955161',// uint64 > PHP_INT_MAX 97 | 'f7' => 3, 98 | 'f8' => 3, 99 | 'f9' => 3, 100 | 'f10' => 3, 101 | 'f11' => -552.339, 102 | 'f12' => -3658.6954, 103 | 'f13' => '-170141183460469231168730371588.4105721', 104 | 'f14' => 3, 105 | 'f15' => str_repeat(md5('aa'), '6'), 106 | 'f16' => '', 107 | 'f17' => md5('55'), 108 | 'f18' => md5('55'), 109 | 'f19' => '2020-09-06', 110 | 'f20' => '2020-09-06', 111 | 'f21' => '2020-09-06 14:25:12', 112 | 'f22' => '2020-09-06 14:25:12.258', 113 | 'f23' => '251.222.221.231', 114 | 'f24' => '192.168.1.2', 115 | 'f25' => '1030::C9B4:FF12:48AA:1A2B', 116 | 'f26' => 'eee22', 117 | 'f27' => [1, 2, 3, 4], 118 | 'f28' => [[['2020-01-05', '2020-01-06']], [['2020-01-07', null], ['2020-01-08']], [['2020-01-09']]], 119 | 'f29' => [[[[[null]]]]] 120 | ], 121 | [ 122 | 'id' => 3, 123 | 'f1' => -1, 124 | 'f2' => 3, 125 | 'f3' => 3, 126 | 'f4' => 3, 127 | 'f5' => 3, 128 | 'f6' => 3, 129 | 'f7' => 3, 130 | 'f8' => 3, 131 | 'f9' => 3, 132 | 'f10' => 3, 133 | 'f11' => 3, 134 | 'f12' => 3, 135 | 'f13' => 3, 136 | 'f14' => 3, 137 | 'f15' => 'aaa', 138 | 'f16' => 'aaa', 139 | 'f17' => md5('a'), 140 | 'f18' => '3026ee79-ac2a-46d2-882d-959a55d71025', 141 | 'f19' => '2020-09-07', 142 | 'f20' => '2020-09-07', 143 | 'f21' => '2020-09-07 14:25:12', 144 | 'f22' => '2020-09-07 14:25:12.258', 145 | 'f23' => '192.168.1.1', 146 | 'f24' => null, 147 | 'f25' => '2001:DB8:2de::e13', 148 | 'f26' => 'eee22', 149 | 'f27' => [12344], 150 | 'f28' => [[['2020-01-05', '2020-01-06'], [null]], [['2020-01-07'], ['2020-01-08']], [['2020-01-09']]], 151 | 'f29' => [[[[['2018-01-25 11:25:14']]]]] 152 | ] 153 | ]); 154 | 155 | 156 | //$data['struct'] = $ck->query('desc t6'); 157 | 158 | 159 | $data['select t6'] = $ck->query('select * from t6'); 160 | 161 | $data['select t6 int64'] = $ck->query("select id,f6 from t6 where f6=1844674407370955161"); 162 | 163 | $data['select t6 Decimal32'] = $ck->query("select id,f11 from t6 where f11='552.339'"); 164 | 165 | $data['select t6 Decimal64'] = $ck->query("select id,f12 from t6 where f12='-3658.69540'"); 166 | 167 | $data['select t6 Decimal128'] = $ck->query("select id,f13 from t6 where f13='170141183460469231168730371588.4105721'"); 168 | 169 | $data['select t6 uuid'] = $ck->query("select id,f18 from t6 where f18='3026ee79-ac2a-46d2-882d-959a55d71025'"); 170 | 171 | $data['select t6 date'] = $ck->query("select id,f19 from t6 where f19='2020-09-05'"); 172 | 173 | $data['select t6 datetime'] = $ck->query("select id,f21 from t6 where f21='2020-09-07 20:25:12'"); 174 | 175 | $data['select t6 datetime64'] = $ck->query("select id,f22 from t6 where f22='2020-09-06 20:25:12.258'"); 176 | 177 | $data['select t6 ip'] = $ck->query("select id,f23,f25 from t6 where f23=" . Types::encodeIpv4('192.168.1.1')); 178 | 179 | $data['select t6 ip64'] = $ck->query("select id,f23,f25 from t6 where f25='" . Types::encodeIpv6('1030::c9b4:ff12:48aa:1a2b') . "'"); 180 | 181 | $data['nothing'] = $ck->query('select array()'); 182 | 183 | $data['drop table'] = $ck->query('DROP TABLE IF EXISTS t5'); 184 | $table = [ 185 | 'CREATE TABLE t5 (', 186 | '`id` UInt32,', 187 | '`f1` UInt16,', 188 | '`f2` SimpleAggregateFunction(max, DateTime64)', 189 | ') ENGINE = AggregatingMergeTree() ORDER BY (id, f1) SETTINGS index_granularity = 8192' 190 | ]; 191 | $data['create table'] = $ck->query(implode("\n", $table)); 192 | 193 | $data['insert data'] = $ck->insert('t5', [ 194 | [ 195 | 'id' => 1, 196 | 'f1' => 1, 197 | 'f2' => '2020-09-29 14:25:12.258' 198 | ], 199 | [ 200 | 'id' => 1, 201 | 'f1' => 1, 202 | 'f2' => '2020-09-29 14:30:56.873' 203 | ], 204 | [ 205 | 'id' => 1, 206 | 'f1' => 1, 207 | 'f2' => '2020-09-29 14:35:46.456' 208 | ], 209 | [ 210 | 'id' => 1, 211 | 'f1' => 1, 212 | 'f2' => '2020-09-29 14:40:16.387' 213 | ], 214 | [ 215 | 'id' => 1, 216 | 'f1' => 1, 217 | 'f2' => '2020-09-29 14:45:22.111' 218 | ] 219 | ]); 220 | 221 | $data['select t5 saf'] = $ck->query("select id,f1,max(f2) from t5 group by id, f1"); 222 | 223 | 224 | $data['drop table'] = $ck->query('DROP TABLE IF EXISTS tt1'); 225 | $ck->query("CREATE TABLE IF NOT EXISTS tt1 ( 226 | some_uuid UUID, 227 | entity_id UInt32, 228 | parameter_id UInt64, 229 | creation_ts SimpleAggregateFunction(max, DateTime64) DEFAULT NOW(), 230 | number SimpleAggregateFunction(sum, UInt64) DEFAULT 1 231 | ) engine = AggregatingMergeTree() 232 | PARTITION BY toYYYYMM(creation_ts) 233 | ORDER BY (some_uuid, entity_id, parameter_id)"); 234 | 235 | $ck->insert('tt1', [ 236 | [ 237 | 'some_uuid' => 'b957bda7-c368-4f43-bc06-640fc6edc466', 238 | 'entity_id' => 4, 239 | 'parameter_id' => 4, 240 | 'number' => 1, 241 | ], 242 | [ 243 | 'some_uuid' => 'b957bda7-c368-4f43-bc06-640fc6edc466', 244 | 'entity_id' => 4, 245 | 'parameter_id' => 7, 246 | 'number' => 1, 247 | ], 248 | [ 249 | 'some_uuid' => 'b957bda7-c368-4f43-bc06-640fc6edc466', 250 | 'entity_id' => 5, 251 | 'parameter_id' => 5, 252 | 'number' => 1, 253 | ], 254 | [ 255 | 'some_uuid' => 'b957bda7-c368-4f43-bc06-640fc6edc466', 256 | 'entity_id' => 6, 257 | 'parameter_id' => 6, 258 | 'number' => 1, 259 | ], 260 | [ 261 | 'some_uuid' => 'b957bda7-c368-4f43-bc06-640fc6edc466', 262 | 'entity_id' => 5, 263 | 'parameter_id' => 5, 264 | 'number' => 1, 265 | ] 266 | ]); 267 | 268 | $tmp_data = $ck->query("SELECT 269 | entity_id, 270 | parameter_id, 271 | sumOrNullIf(number, some_uuid NOT IN ('b957bda7-c368-4f43-bc06-640fc6edc466')) AS number_0, 272 | maxOrNullIf(creation_ts, some_uuid NOT IN ('b957bda7-c368-4f43-bc06-640fc6edc466')) as creation_ts_0, 273 | sumOrNullIf(number, some_uuid NOT IN ('c34b4e9f-690e-4f24-b67c-206242a4287c')) AS number_1, 274 | maxOrNullIf(creation_ts, some_uuid NOT IN ('c34b4e9f-690e-4f24-b67c-206242a4287c')) as creation_ts_1 275 | FROM tt1 276 | WHERE some_uuid IN ('b957bda7-c368-4f43-bc06-640fc6edc466','c34b4e9f-690e-4f24-b67c-206242a4287c') 277 | GROUP BY entity_id, parameter_id 278 | ORDER BY entity_id"); 279 | foreach ($tmp_data as $v) { 280 | if ($v['number_1'] == null || $v['creation_ts_1'] == null) { 281 | throw new \Exception('test fail line:' . __LINE__); 282 | } 283 | } 284 | 285 | // flow of write 286 | $data['drop table'] = $ck->query('DROP TABLE IF EXISTS t7'); 287 | $table = [ 288 | 'CREATE TABLE t7 (', 289 | '`id` UInt32,', 290 | '`f2` Nullable(Int32),', 291 | '`f5` UInt16,', 292 | '`f15` String', 293 | ') ENGINE = MergeTree() ORDER BY id SETTINGS index_granularity = 8192' 294 | ]; 295 | $data['create table'] = $ck->query(implode("\n", $table)); 296 | $ck->writeStart('t7', ['id', 'f2', 'f5', 'f15']); 297 | for ($i = 0; $i < 1000; $i++) { 298 | $da = []; 299 | for ($j = 0; $j < 1000; $j++) { 300 | $da[] = [ 301 | 'id' => mt_rand(1, 1000000), 302 | 'f2' => mt_rand(-1000000, 1000000), 303 | 'f5' => mt_rand(1, 10000), 304 | 'f15' => md5(mt_rand(1, 10000)) 305 | ]; 306 | } 307 | $ck->writeBlock($da); 308 | } 309 | 310 | $ck->writeEnd(); 311 | 312 | $data['write 100w rows time'] = microtime(true) - $t1; 313 | 314 | echo json_encode($data); 315 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | ['tcp_nodelay' => true]] 46 | ) : null; 47 | 48 | $flags = isset($options['persistent']) && !empty($options['persistent']) ? 49 | STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT 50 | : STREAM_CLIENT_CONNECT; 51 | 52 | $this->conn = $context ? stream_socket_client( 53 | $dsn, 54 | $code, 55 | $msg, 56 | $options['connect_timeout'] ?? 3, 57 | $flags, 58 | $context 59 | ) : stream_socket_client( 60 | $dsn, 61 | $code, 62 | $msg, 63 | $options['connect_timeout'] ?? 3, 64 | $flags 65 | ); 66 | 67 | if (!$this->conn) { 68 | throw new CkException($msg, $code); 69 | } 70 | 71 | stream_set_timeout($this->conn, $options['socket_timeout'] ?? 30); 72 | $this->write = new Write($this->conn); 73 | $this->read = new Read($this->conn); 74 | $this->types = new Types($this->write, $this->read); 75 | $this->conf = [$username, $password, $database]; 76 | $this->hello(...$this->conf); 77 | } 78 | 79 | public function __destruct() 80 | { 81 | if (is_resource($this->conn)) { 82 | stream_socket_shutdown($this->conn, STREAM_SHUT_RDWR); 83 | fclose($this->conn); 84 | } 85 | } 86 | 87 | private function addClientInfo() 88 | { 89 | $this->write->string(self::NAME)->number(self::VERSION_MAJOR, self::VERSION_MINOR, self::VERSION); 90 | } 91 | 92 | private function hello($username, $password, $database) 93 | { 94 | $this->write->number(Protocol::CLIENT_HELLO); 95 | $this->addClientInfo(); 96 | $this->write->string($database, $username, $password); 97 | $this->write->flush(); 98 | return $this->receive(); 99 | } 100 | 101 | /** 102 | * @return bool 103 | */ 104 | public function ping() 105 | { 106 | $this->write->number(Protocol::CLIENT_PING); 107 | $this->write->flush(); 108 | return $this->receive(); 109 | } 110 | 111 | private $_server_info = []; 112 | private $_row_data = []; 113 | private $_total_row = 0; 114 | private $fields = []; 115 | 116 | 117 | /** 118 | * @return array|bool 119 | */ 120 | private function receive() 121 | { 122 | $this->_row_data = []; 123 | $this->_total_row = 0; 124 | $this->fields = []; 125 | $_progress_info = []; 126 | $_profile_info = []; 127 | 128 | $code = null; 129 | do { 130 | if ($code === null) { 131 | $code = $this->read->number(); 132 | } 133 | switch ($code) { 134 | case Protocol::SERVER_HELLO: 135 | $this->setServerInfo(); 136 | return true; 137 | case Protocol::SERVER_EXCEPTION: 138 | $this->readErr(); 139 | break; 140 | case Protocol::SERVER_DATA: 141 | $n = $this->readData(); 142 | if ($n > 1) { 143 | $code = $n; 144 | } 145 | continue 2; 146 | case Protocol::SERVER_PROGRESS: 147 | $_progress_info = [ 148 | 'rows' => $this->read->number(), 149 | 'bytes' => $this->read->number(), 150 | 'total_rows' => $this->gtV(self::DBMS_MIN_V_TOTAL_ROWS_IN_PROGRESS) ? $this->read->number() : 0, 151 | ]; 152 | break; 153 | case Protocol::SERVER_END_OF_STREAM: 154 | return $this->_row_data; 155 | // return [ 156 | // 'total_row' => $this->_total_row, 157 | // 'data' => $this->_row_data, 158 | // 'field' => $this->fields, 159 | // 'progress_info' => $_progress_info, 160 | // 'profile_info' => $_profile_info, 161 | // ]; 162 | case Protocol::SERVER_PROFILE_INFO: 163 | $_profile_info = [ 164 | 'rows' => $this->read->number(), 165 | 'blocks' => $this->read->number(), 166 | 'bytes' => $this->read->number(), 167 | 'applied_limit' => $this->read->number(), 168 | 'rows_before_limit' => $this->read->number(), 169 | 'calculated_rows_before_limit' => $this->read->number() 170 | ]; 171 | break; 172 | case Protocol::SERVER_TOTALS: 173 | case Protocol::SERVER_EXTREMES: 174 | throw new CkException('Report to me this error ' . $code, CkException::CODE_UNDO); 175 | break; 176 | case Protocol::SERVER_PONG: 177 | return true; 178 | default: 179 | throw new CkException('undefined code ' . $code, CkException::CODE_UNDEFINED); 180 | } 181 | $code = null; 182 | } while (true); 183 | } 184 | 185 | private function gtV($v) 186 | { 187 | return $this->_server_info['version'] >= $v; 188 | } 189 | 190 | 191 | /** 192 | * @return array 193 | */ 194 | public function getServerInfo() 195 | { 196 | return $this->_server_info; 197 | } 198 | 199 | 200 | private function readData() 201 | { 202 | if (count($this->fields) === 0) { 203 | $this->readHeader(); 204 | } 205 | list($code, $row_count) = $this->readHeader(); 206 | if ($row_count === 0) { 207 | return $code; 208 | } 209 | foreach ($this->fields as $t) { 210 | $f = $this->read->string(); 211 | $t = $this->read->string(); 212 | $col = $this->types->unpack($t, $row_count); 213 | $i = 0; 214 | foreach ($col as $el) { 215 | $this->_row_data[$i + $this->_total_row][$f] = $el; 216 | $i++; 217 | } 218 | } 219 | $this->_total_row += $row_count; 220 | return 1; 221 | } 222 | 223 | private function readHeader() 224 | { 225 | $n = $this->read->number(); 226 | if ($n > 1) { 227 | return [$n, 0]; 228 | } 229 | $info = [ 230 | 'num1' => $this->read->number(), 231 | 'is_overflows' => $this->read->number(), 232 | 'num2' => $this->read->number(), 233 | 'bucket_num' => $this->read->int(), 234 | 'num3' => $this->read->number(), 235 | 'col_count' => $this->read->number(), 236 | 'row_count' => $this->read->number(), 237 | ]; 238 | if (count($this->fields) === 0) { 239 | for ($i = 0; $i < $info['col_count']; $i++) { 240 | $this->fields[$this->read->string()] = $this->read->string(); 241 | } 242 | } 243 | return [0, $info['row_count']]; 244 | } 245 | 246 | private function setServerInfo() 247 | { 248 | $this->_server_info = [ 249 | 'name' => $this->read->string(), 250 | 'major_version' => $this->read->number(), 251 | 'minor_version' => $this->read->number(), 252 | 'version' => $this->read->number(), 253 | ]; 254 | $this->_server_info['time_zone'] = $this->gtV(self::DBMS_MIN_V_SERVER_TIMEZONE) ? $this->read->string() : ''; 255 | } 256 | 257 | 258 | private function sendQuery($sql) 259 | { 260 | $this->write->number(Protocol::CLIENT_QUERY, 0); 261 | 262 | if ($this->gtV(self::DBMS_MIN_V_CLIENT_INFO)) { 263 | 264 | // query kind 265 | $this->write->number(1) 266 | // name, id, ip 267 | ->string('', '', '[::ffff:127.0.0.1]:0') 268 | // iface type tcp, os ser, hostname 269 | ->number(1)->string('', ''); 270 | 271 | $this->addClientInfo(); 272 | 273 | if ($this->gtV(self::DBMS_MIN_V_QUOTA_KEY_IN_CLIENT_INFO)) { 274 | $this->write->string(''); 275 | } 276 | 277 | } 278 | 279 | $this->write->number(0, Protocol::STAGES_COMPLETE, Protocol::COMPRESSION_DISABLE)->string($sql); 280 | 281 | } 282 | 283 | /** 284 | * @param string $sql 285 | */ 286 | public function query($sql) 287 | { 288 | $this->fields = []; 289 | $this->sendQuery($sql); 290 | return $this->writeEnd(); 291 | } 292 | 293 | /** 294 | * @param string $table 295 | * @param string[][] $data 296 | * @return array|bool 297 | * @throws CkException 298 | */ 299 | public function insert($table, $data) 300 | { 301 | $this->writeStart($table, array_keys($data[0])); 302 | $this->writeBlock($data); 303 | return $this->writeEnd(); 304 | } 305 | 306 | /** 307 | * @param string $table 308 | * @param string[] $fields 309 | * @throws CkException 310 | */ 311 | public function writeStart($table, $fields) 312 | { 313 | $this->fields = []; 314 | $table = trim($table); 315 | $this->sendQuery('INSERT INTO ' . $table . ' (' . implode(',', $fields) . ') VALUES '); 316 | $this->writeEnd(false); 317 | while (true) { 318 | $code = $this->read->number(); 319 | if ($code == Protocol::SERVER_DATA) { 320 | $this->readHeader(); 321 | break; 322 | } else if ($code == Protocol::SERVER_PROGRESS) { 323 | continue; 324 | } else if ($code == Protocol::SERVER_EXCEPTION) { 325 | $this->readErr(); 326 | } else { 327 | throw new CkException('insert err code:' . $code, CkException::CODE_INSERT_ERR); 328 | } 329 | } 330 | } 331 | 332 | /** 333 | * @param string[][] $data 334 | * @throws CkException 335 | */ 336 | public function writeBlock($data) 337 | { 338 | if (count($this->fields) === 0) { 339 | throw new CkException('Please execute first writeStart', CkException::CODE_TODO_WRITE_START); 340 | } 341 | $this->writeBlockHead(); 342 | 343 | // column count , row Count 344 | $row_count = count($data); 345 | $this->write->number(count($data[0]), $row_count); 346 | 347 | $new_data = []; 348 | foreach ($data as $row) { 349 | foreach ($row as $k => $v) { 350 | $new_data[$k][] = $v; 351 | } 352 | } 353 | 354 | foreach ($new_data as $field => $data) { 355 | $type = $this->fields[$field]; 356 | $this->write->string($field, $type); 357 | $this->types->pack($data, $type); 358 | $this->write->flush(); 359 | } 360 | $this->write->flush(); 361 | 362 | } 363 | 364 | /** 365 | * @param false $get_ret 366 | * @return array|bool 367 | * @throws CkException 368 | */ 369 | public function writeEnd($get_ret = true) 370 | { 371 | $this->writeBlockHead(); 372 | $this->write->number(0); 373 | $this->write->number(0); 374 | $this->write->flush(); 375 | if ($get_ret === true) { 376 | return $this->receive(); 377 | } 378 | } 379 | 380 | private function writeBlockHead() 381 | { 382 | $this->write->number(Protocol::CLIENT_DATA); 383 | if ($this->gtV(self::DBMS_MIN_V_TEMPORARY_TABLES)) { 384 | $this->write->number(0); 385 | } 386 | if ($this->gtV(self::DBMS_MIN_V_BLOCK_INFO)) { 387 | $this->write->number(1, 0, 2); 388 | $this->write->int(-1); 389 | $this->write->number(0); 390 | } 391 | } 392 | 393 | private function readErr() 394 | { 395 | $c = $this->read->int(); 396 | $n = $this->read->string(); 397 | $msg = $this->read->string(); 398 | throw new CkException(substr($msg, strlen($n) + 1), $c); 399 | } 400 | 401 | } -------------------------------------------------------------------------------- /src/Types.php: -------------------------------------------------------------------------------- 1 | ['c', 1], 27 | 'uint8' => ['C', 1], 28 | 'int16' => ['s', 2], 29 | 'uint16' => ['S', 2], 30 | 'int32' => ['l', 4], 31 | 'uint32' => ['L', 4], 32 | 'int64' => ['q', 8], 33 | // 'uint64' => ['Q', 8], 34 | 'float32' => ['f', 4], 35 | 'float64' => ['d', 8] 36 | ]; 37 | 38 | const ALIAS_TYPES = [ 39 | 'decimal32' => 'float32', 40 | 'decimal64' => 'float64', 41 | 'date' => 'uint16', 42 | 'datetime' => 'uint32', 43 | 'ipv4' => 'uint32', 44 | 'ipv6' => 'fixedstring(16)', 45 | 'enum8' => 'int8', 46 | 'enum16' => 'int16', 47 | 'nothing' => 'int8', 48 | 'bool' => 'uint8' 49 | ]; 50 | 51 | public function __construct($write, $read) 52 | { 53 | $this->write = $write; 54 | $this->read = $read; 55 | } 56 | 57 | 58 | public static function encodeIpv4($ip) 59 | { 60 | return ip2long($ip); 61 | } 62 | 63 | public static function encodeIpv6($ip) 64 | { 65 | $ar = explode(':', $ip); 66 | if (count($ar) < 8 || strpos($ip, '::')) { 67 | $r = []; 68 | foreach ($ar as $v) { 69 | if (empty($v)) { 70 | $r = array_merge($r, array_fill(0, 9 - count($ar), '0000')); 71 | continue; 72 | } 73 | $r[] = str_pad($v, 4, '0', STR_PAD_LEFT); 74 | } 75 | $ar = $r; 76 | } 77 | return hex2bin(implode($ar)); 78 | } 79 | 80 | public static function encodeFixedString($str, $n) 81 | { 82 | return str_pad($str, $n, chr(0)); 83 | } 84 | 85 | public static function encodeDate($date) 86 | { 87 | return ceil(strtotime($date) / 86400); 88 | } 89 | 90 | public static function encodeDatetime($datetime) 91 | { 92 | return strtotime($datetime); 93 | } 94 | 95 | public static function encodeDatetime64($time, $n = 3) 96 | { 97 | $ar = explode('.', $time); 98 | $l = isset($ar[1]) ? strlen($ar[1]) : 0; 99 | $n = strtotime($ar[0]) . (isset($ar[1]) ? $ar[1] : '') . str_repeat('0', min(max($n - $l, 0), 9)); 100 | return $n * 1; 101 | } 102 | 103 | public static function encodeUuid($data) 104 | { 105 | $s = str_replace('-', '', $data); 106 | $r1 = substr($s, 0, 8); 107 | $r2 = substr($s, 8, 8); 108 | $r3 = substr($s, 16, 8); 109 | $r4 = substr($s, 24); 110 | return pack('L4', hexdec($r2), hexdec($r1), hexdec($r4), hexdec($r3)); 111 | } 112 | 113 | /** 114 | * @param string $n 115 | * @return string 116 | */ 117 | public static function encodeInt128($n) 118 | { 119 | if (!is_string($n)) { 120 | $n = "{$n}"; 121 | } 122 | $is_n = false; 123 | if ($n[0] === '-') { 124 | $is_n = true; 125 | } 126 | $r = ''; 127 | for ($i = 0; $i < 16; $i++) { 128 | $b = bcpow(2, 8); 129 | $c = bcmod($n, $b); 130 | $n = bcdiv($n, $b, 0); 131 | if ($is_n) { 132 | $v = ~abs(intval($c)); 133 | if ($i === 0) { 134 | $v = $v + 1; 135 | } 136 | } else { 137 | $v = intval($c); 138 | } 139 | $r .= chr($v); 140 | } 141 | return $r; 142 | } 143 | 144 | protected function encodeUint64($str) 145 | { 146 | $str = "{$str}"; 147 | return pack('L2', intval(bcmod($str, '4294967296')), intval(bcdiv($str, '4294967296'))); 148 | } 149 | 150 | 151 | protected function decodeUint64() 152 | { 153 | $str = $this->read->getChar(8); 154 | $r = unpack('L*', $str); 155 | return bcadd(bcmul($r[2], bcpow(2, 32)), $r[1]); 156 | } 157 | 158 | 159 | protected function decodeUuid() 160 | { 161 | $s = bin2hex($this->read->getChar(8)); 162 | $r = ''; 163 | for ($i = 14; $i >= 0; $i -= 2) { 164 | $r .= $s[$i] . $s[$i + 1]; 165 | } 166 | $r = substr($r, 0, 8) . '-' . substr($r, 8, 4) . '-' . substr($r, 12); 167 | 168 | $s = bin2hex($this->read->getChar(8)); 169 | $r1 = ''; 170 | for ($i = 14; $i >= 0; $i -= 2) { 171 | $r1 .= $s[$i] . $s[$i + 1]; 172 | } 173 | $r .= '-' . substr($r1, 0, 4) . '-' . substr($r1, 4); 174 | return $r; 175 | } 176 | 177 | 178 | /** 179 | * @return string 180 | */ 181 | protected function decodeInt128() 182 | { 183 | $str = $this->read->getChar(16); 184 | $is_n = false; 185 | if (ord($str[15]) > 127) { 186 | $is_n = true; 187 | } 188 | $r = '0'; 189 | for ($i = 0; $i < 16; $i++) { 190 | $n = ord($str[$i]); 191 | if ($is_n) { 192 | if ($i === 0) { 193 | $n = $n - 1; 194 | } 195 | $n = ~$n & 255; 196 | } 197 | if ($n !== 0) { 198 | $r = bcadd(bcmul("{$n}", bcpow(2, 8 * $i)), $r); 199 | } 200 | } 201 | return $is_n ? '-' . $r : $r; 202 | } 203 | 204 | protected function decodeIpv6($data) 205 | { 206 | $s = bin2hex($data); 207 | $r = []; 208 | $a = ''; 209 | for ($i = 0; $i < 32; $i++) { 210 | $a .= $s[$i]; 211 | if ($i < 31 && $i % 4 === 3) { 212 | $r[] = ltrim($a, '0'); 213 | $a = ''; 214 | } 215 | } 216 | $r[] = ltrim($a, '0'); 217 | $r = implode(':', $r); 218 | while (strpos($r, ':::') !== false) { 219 | $r = str_replace(':::', '::', $r); 220 | } 221 | return $r; 222 | } 223 | 224 | 225 | public static function isDecimal($str) 226 | { 227 | return strpos($str, 'decimal(') === 0; 228 | } 229 | 230 | public static function isDatetime64($str) 231 | { 232 | return strpos($str, 'datetime64(') === 0; 233 | } 234 | 235 | 236 | public static function isArray($str) 237 | { 238 | return strpos($str, 'array(') === 0; 239 | } 240 | 241 | 242 | public static function isNullable($str) 243 | { 244 | return strpos($str, 'nullable(') === 0; 245 | } 246 | 247 | public static function isFixedString($str) 248 | { 249 | return strpos($str, 'fixedstring(') === 0; 250 | } 251 | 252 | public static function isSimpleAggregateFunction($str) 253 | { 254 | return strpos($str, 'simpleaggregatefunction(') === 0; 255 | } 256 | 257 | 258 | protected function alias(&$tp) 259 | { 260 | $type = $tp; 261 | if (isset(self::BASE_TYPE[$type]) || $type === 'string' || self::isFixedString($type)) { 262 | return $type; 263 | } 264 | if (isset(self::ALIAS_TYPES[$type])) { 265 | return self::ALIAS_TYPES[$type]; 266 | } 267 | if (self::isNullable($type)) { 268 | $this->is_null = true; 269 | $tp = substr($type, 9, -1); 270 | return $this->alias($tp); 271 | } 272 | if (self::isDecimal($type)) { 273 | $arr = explode(',', substr($type, 8, -1)); 274 | if ($arr[0] < 10) { 275 | return 'int32'; 276 | } else if ($arr[0] < 19) { 277 | return 'int64'; 278 | } else { 279 | return 'int128'; 280 | } 281 | } 282 | if (self::isDatetime64($type)) { 283 | return 'uint64'; 284 | } 285 | if (self::isSimpleAggregateFunction($type)) { 286 | $tp = substr(trim(strstr($type, ','), ' ,'), 0, -1); 287 | $type = $tp; 288 | return $this->alias($type); 289 | } 290 | $is_arr = false; 291 | while (self::isArray($type)) { 292 | $this->arr_dp[] = 'array'; 293 | $type = substr($type, 6, -1); 294 | $is_arr = true; 295 | } 296 | if ($is_arr) { 297 | $this->arr_type = $type; 298 | return $this->alias($this->arr_type); 299 | } 300 | return $type; 301 | } 302 | 303 | 304 | protected function getArrData($row_count, $real_type) 305 | { 306 | $deep = count($this->arr_dp); 307 | $data = array_fill(0, $row_count, []); 308 | $arr = []; 309 | $els = []; 310 | $first = true; 311 | while ($deep--) { 312 | $del = []; 313 | $l = count($data); 314 | $p = 0; 315 | foreach ($data as $i => &$val) { 316 | if ($first) { 317 | $arr[] = &$val; 318 | } 319 | $num = unpack('Q', $this->read->getChar(8))[1]; //array len is uint64 -> PHP_INT_MAX 320 | $val = array_fill(0, $num - $p, []); 321 | $p = $num; 322 | foreach ($val as &$v) { 323 | if ($deep > 0) { 324 | $data[] = &$v; 325 | } else { 326 | $els[] = &$v; 327 | } 328 | } 329 | $del[] = $i; 330 | $l--; 331 | if ($l === 0) { 332 | break; 333 | } 334 | } 335 | foreach ($del as $i) { 336 | unset($data[$i]); 337 | } 338 | $first = false; 339 | } 340 | 341 | $row_count = count($els); 342 | $this->getNull($row_count); 343 | $this->decode($real_type, $row_count); 344 | foreach ($this->is_null_data as $i => $v) { 345 | $this->col_data[$i] = null; 346 | } 347 | $this->unFormat($this->arr_type); 348 | foreach ($els as $i => &$v) { 349 | $v = $this->col_data[$i]; 350 | } 351 | $this->col_data = []; 352 | $this->arr_dp = []; 353 | 354 | return isset($els[0]) ? $arr : []; 355 | } 356 | 357 | protected function setArrData($in_da, $type, $real_type) 358 | { 359 | $data = []; 360 | $index = [$in_da]; 361 | $r = []; 362 | $arr_dp = 0; 363 | while (true) { 364 | $del = []; 365 | $j = 0; 366 | foreach ($index as $i => $val) { 367 | $j += count($val); 368 | $r[] = $j; 369 | $del[] = $i; 370 | if (isset($val[0]) && is_array($val[0])) { 371 | foreach ($val as $v) { 372 | $index[] = $v; 373 | } 374 | } else { 375 | $data = array_merge($data, $val); 376 | } 377 | } 378 | foreach ($del as $i) { 379 | unset($index[$i]); 380 | } 381 | if (empty($index)) { 382 | break; 383 | } 384 | $arr_dp++; 385 | } 386 | 387 | if (count($this->arr_dp) !== $arr_dp) { 388 | throw new CkException('array deep err', CkException::CODE_ARR_ERR); 389 | } 390 | array_shift($r); 391 | $this->write->addBuf(pack('Q' . '*', ...$r)); // array len is uint64 -> PHP_INT_MAX 392 | $this->setNull($data); 393 | $this->format($data, $this->arr_type); 394 | $this->encode($data, $type, $real_type); 395 | 396 | $this->arr_dp = []; 397 | 398 | } 399 | 400 | 401 | protected function getNull($row_count) 402 | { 403 | $this->is_null_data = []; 404 | if ($this->is_null) { 405 | for ($i = 0; $i < $row_count; $i++) { 406 | $n = $this->read->number(); 407 | if ($n === 1) { 408 | $this->is_null_data[$i] = 1; 409 | } 410 | } 411 | } 412 | } 413 | 414 | /** 415 | * @param $data 416 | */ 417 | protected function setNull(&$data) 418 | { 419 | if ($this->is_null) { 420 | foreach ($data as $i => &$v) { 421 | if ($v === null) { 422 | $this->is_null_data[$i] = 1; 423 | $v = 0; 424 | } else { 425 | $this->is_null_data[$i] = 0; 426 | } 427 | } 428 | $this->write->addBuf(pack('C*', ...$this->is_null_data)); 429 | } 430 | $this->is_null_data = []; 431 | } 432 | 433 | 434 | /** 435 | * @param string[] $data 436 | * @param $type 437 | * @return mixed|null 438 | */ 439 | protected function format(&$data, $type) 440 | { 441 | if (isset(self::BASE_TYPE[$type]) || $type === 'string') { 442 | return 1; 443 | } 444 | $call = [ 445 | 'date' => function ($v) { 446 | return self::encodeDate($v); 447 | }, 448 | 'datetime' => function ($v) { 449 | return self::encodeDatetime($v); 450 | }, 451 | 'ipv4' => function ($v) { 452 | return self::encodeIpv4($v); 453 | }, 454 | 'ipv6' => function ($v) { 455 | return self::encodeIpv6($v); 456 | } 457 | ]; 458 | $fn = null; 459 | if (isset($call[$type])) { 460 | $fn = $call[$type]; 461 | } else if (self::isDecimal($type)) { 462 | $arr = explode(',', substr($type, 8, -1)); 463 | $scale = intval($arr[1]); 464 | $fn = function ($v) use ($scale) { 465 | return bcmul($v, bcpow('10', $scale, 0), 0); 466 | }; 467 | } else if (self::isDatetime64($type)) { 468 | $n = substr($type, 11, -1); 469 | $fn = function ($v) use ($n) { 470 | return self::encodeDatetime64($v, $n); 471 | }; 472 | } 473 | 474 | if ($fn) { 475 | foreach ($data as &$el) { 476 | if ($el !== null) { 477 | $el = $fn($el); 478 | } 479 | } 480 | } 481 | } 482 | 483 | 484 | protected function unFormat($type) 485 | { 486 | if (isset(self::BASE_TYPE[$type]) || $type === 'string' || $type === 'uuid' || self::isFixedString($type) || $type === 'nothing') { 487 | return 1; 488 | } 489 | 490 | $call = [ 491 | 'date' => function ($v) { 492 | return date('Y-m-d', $v * 86400); 493 | }, 494 | 'datetime' => function ($v) { 495 | return date('Y-m-d H:i:s', $v); 496 | }, 497 | 'ipv4' => function ($v) { 498 | return long2ip($v); 499 | }, 500 | 'ipv6' => function ($v) { 501 | return $this->decodeIpv6($v); 502 | } 503 | ]; 504 | 505 | $fn = null; 506 | if (isset($call[$type])) { 507 | $fn = $call[$type]; 508 | } else if (self::isDecimal($type)) { 509 | $arr = explode(',', substr($type, 8, -1)); 510 | $scale = intval($arr[1]); 511 | $fn = function ($v) use ($scale) { 512 | return bcdiv($v, bcpow('10', $scale, 0), $scale); 513 | }; 514 | } else if (self::isDatetime64($type)) { 515 | $fn = function ($v) { 516 | return date('Y-m-d H:i:s', substr($v, 0, 10)) . '.' . substr($v, 10); 517 | }; 518 | } 519 | 520 | if ($fn === null) { 521 | return 1; 522 | } 523 | 524 | foreach ($this->col_data as &$el) { 525 | if ($el !== null) { 526 | $el = $fn($el); 527 | } 528 | } 529 | 530 | } 531 | 532 | 533 | /** 534 | * @param $type 535 | * @return mixed|string 536 | * @throws CkException 537 | */ 538 | protected function decode($type, $row_count) 539 | { 540 | if ($row_count === 0) { 541 | return 1; 542 | } 543 | 544 | if (isset(self::BASE_TYPE[$type])) { 545 | $this->col_data = array_values( 546 | unpack(self::BASE_TYPE[$type][0] . '*', 547 | $this->read->getChar(self::BASE_TYPE[$type][1] * $row_count)) 548 | ); 549 | return 1; 550 | } 551 | $fn = null; 552 | if ($type === 'string') { 553 | $fn = function () { 554 | return $this->read->string(); 555 | }; 556 | } else if ($type === 'uint64') { 557 | $fn = function () { 558 | return $this->decodeUint64(); 559 | }; 560 | } else if (self::isFixedString($type)) { 561 | $n = intval(substr($type, 12, -1)); 562 | $fn = function () use ($n) { 563 | return $this->read->getChar($n); 564 | }; 565 | } else if ($type === 'int128') { 566 | $fn = function () { 567 | return $this->decodeInt128(); 568 | }; 569 | } else if ($type === 'uuid') { 570 | $fn = function () { 571 | return $this->decodeUuid(); 572 | }; 573 | } else { 574 | throw new CkException('not supported type :' . $type, CkException::CODE_NOT_SUPPORTED_TYPE); 575 | } 576 | $this->col_data = []; 577 | for ($i = 0; $i < $row_count; $i++) { 578 | $this->col_data[] = $fn(); 579 | } 580 | 581 | } 582 | 583 | /** 584 | * @param string[] $data 585 | * @param string $type 586 | * @param string $real_type 587 | * @throws CkException 588 | */ 589 | protected function encode($data, $type, $real_type) 590 | { 591 | if (isset(self::BASE_TYPE[$real_type])) { 592 | $this->write->addBuf(pack(self::BASE_TYPE[$real_type][0] . '*', ...$data)); 593 | return 1; 594 | } 595 | $fn = null; 596 | if ($real_type === 'string') { 597 | $fn = function ($v) { 598 | $this->write->string($v); 599 | }; 600 | } else if ($real_type === 'uint64') { 601 | $fn = function ($v) { 602 | $this->write->addBuf($this->encodeUint64($v)); 603 | }; 604 | } else if (self::isFixedString($real_type)) { 605 | $n = intval(substr($real_type, 12, -1)); 606 | $fn = function ($v) use ($n) { 607 | $this->write->addBuf(self::encodeFixedString($v, $n)); 608 | }; 609 | } else if ($real_type === 'int128') { 610 | $fn = function ($v) { 611 | $this->write->addBuf(self::encodeInt128($v)); 612 | }; 613 | } else if ($real_type === 'uuid') { 614 | $fn = function ($v) { 615 | $this->write->addBuf(self::encodeUuid($v)); 616 | }; 617 | } else { 618 | throw new CkException('unset type :' . $type, CkException::CODE_UNSET_TYPE); 619 | } 620 | foreach ($data as $el) { 621 | $fn($el); 622 | } 623 | } 624 | 625 | 626 | public function unpack($type, $row_count) 627 | { 628 | $type = strtolower($type); 629 | $this->is_null = false; 630 | $real_type = $this->alias($type); 631 | if (isset($this->arr_dp[0])) { 632 | return $this->getArrData($row_count, $real_type); 633 | } else { 634 | $this->getNull($row_count); 635 | $this->decode($real_type, $row_count); 636 | foreach ($this->is_null_data as $i => $v) { 637 | $this->col_data[$i] = null; 638 | } 639 | $this->unFormat($type); 640 | return $this->col_data; 641 | } 642 | } 643 | 644 | public function pack($data, $type) 645 | { 646 | $type = strtolower($type); 647 | $this->is_null = false; 648 | $real_type = $this->alias($type); 649 | $this->format($data, $type); 650 | if (isset($this->arr_dp[0])) { 651 | $this->setArrData($data, $type, $real_type); 652 | } else { 653 | $this->setNull($data); 654 | $this->encode($data, $type, $real_type); 655 | } 656 | } 657 | 658 | } 659 | --------------------------------------------------------------------------------