├── .travis.yml ├── CONTRIBUTING.md ├── LEGAL ├── LICENSE ├── Makefile ├── README.md ├── ebin └── .gitignore ├── example.erl ├── include ├── sql.hrl └── sqlapi.hrl ├── nanomysql.erl ├── rebar ├── src ├── my_datatypes.erl ├── my_packet.erl ├── my_protocol.erl ├── my_ranch_worker.erl ├── myproto.hrl ├── nanomysql.erl ├── sql92.erl ├── sql92_parser.erl ├── sql92_parser.yrl ├── sql92_scan.erl ├── sql92_scan.xrl ├── sql_ets_api.erl ├── sqlapi.app.src ├── sqlapi.erl ├── sqlapi_app.erl ├── sqlapi_ram_table.erl └── sqlapi_sup.erl └── test ├── .gitignore ├── sql_SUITE.erl ├── sql_ets_api_SUITE.erl └── test_handler.erl /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | script : "make && make test" 3 | otp_release: 4 | - 20.1 5 | - 20.0 6 | - 19.0 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We are glad to accept changes that make this piece of code better. 2 | 3 | By offering your code you assume that it will be licensed under MIT license and available for other people. 4 | 5 | Thanks! 6 | -------------------------------------------------------------------------------- /LEGAL: -------------------------------------------------------------------------------- 1 | 2 | All source code in this repository except files listed belows has copyright of Erlyvideo LLC and is distributed under MIT license. 3 | 4 | You can use it for commercial or non-commercial purpose without any warranty. Do not come to us if this code injures anybody, 5 | blow a reactor, crashes a plane or do some any harm: this is AS IS. 6 | 7 | 8 | Following files are distributed under EPL (erlang public license): 9 | 10 | include/sql.hrl 11 | src/my_datatypes.erl 12 | src/my_packet.erl 13 | src/myproto.hrl 14 | test/sql_SUITE.erl 15 | 16 | 17 | Thanks a lot to Manuel Rubio for writing initial implementation of MySQL protocol and remember, 18 | that his code is also distributed AS IS + it is EPL, so do not close this source. 19 | 20 | Take a look at his code at https://github.com/altenwald/myproto It is under LGPL there but we have took it when it was under EPL. 21 | 22 | 23 | Also this repository has code distributed by MIT from third party: 24 | 25 | src/sql92_parser.yrl (and generated from it src/sql92_parser.erl) 26 | src/sql92_scan.xrl (and generated from it src/sql92_scan.erl) 27 | 28 | Many thanks to Oleg Smirnov and his work https://github.com/master/mongosql for this 29 | fast and convenient SQL parser 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Erlyvideo, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | ./rebar compile 3 | 4 | clean: 5 | ./rebar clean 6 | 7 | test: 8 | mkdir -p logs 9 | ct_run -noshell -pa ebin -logdir logs/ 10 | 11 | 12 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | [![Build Status](https://api.travis-ci.org/flussonic/sqlapi.png)](https://travis-ci.org/flussonic/sqlapi) 5 | 6 | 7 | This library helps you to create an erlang application into a SQL server. 8 | 9 | You will be able to access internal of running erlang server as if it is a SQL database with tables. 10 | 11 | 12 | 13 | 14 | Why not just HTTP 15 | ----------------- 16 | 17 | This is a replacement to usual HTTP JSON/XML API and can be more convenient because SQL gives you 18 | sorting, paging, filtering out of the box and HTTP API usually has all this designed ad-hoc. 19 | 20 | 21 | This piece of code handles `ORDER BY`, `LIMIT`, `OFFSET`, etc, out of the box. 22 | 23 | 24 | Main idea was to make it accessible from modern ORMs like Ruby on Rails, Flask and so on. 25 | 26 | It happened to be very convenient, because ORM engines are usually much better developed and supported 27 | than ad-hoc implementation of http api library that has to be rewritten for each service and protocol. 28 | 29 | 30 | 31 | 32 | How it differs from myproto 33 | --------------------------- 34 | 35 | 36 | Our `sqlapi` projects relies on parser of mysql protocol. Not a best solution, very sorry, once we will add postgresql 37 | support. `https://github.com/altenwald/myproto` was selected as an implementation of wire protocol that 38 | can decode and encode packets and parse SQL. We had to modify it a lot and remove some pieces of code, 39 | so now there is not too much from `myproto` (see LEGAL file). Mostly we had to add lot of 40 | code that implements behaviour of MySQL server. 41 | 42 | `myproto` only parses queries, but we need to handle them and reply. While adding support for more and more ORM 43 | we have met different amazing ways to read the same data. For example, each ORM has it's own unique way to get 44 | list of tables and columns. It is not a problem of ORM's, it is a problem of MySQL that allows so many undocumented 45 | ways to get data that anyone requires. This is why we want once to migrate to postgresql protocol and forget about mysql. 46 | 47 | Our code offers a lot of default implementations of queries like `SHOW FIELDS` or `SELECT @@global.max_allowed_packet` 48 | All these queries are useless outside of MySQL server, but many ORM will not work if you do not reply properly on these 49 | requests. 50 | 51 | 52 | It is important to understand that while implementing this `sqlapi` we had to make a compromise between big list of 53 | possible features available in SQL and simplicity of usage. Simplicity was selected. 54 | For example you cannot handle `CREATE DATABASE` or `SET @key='value'` 55 | 56 | 57 | 58 | How to use it 59 | ------------- 60 | 61 | You need to write your own handler that will handle business logic. 62 | 63 | We have made an example called `sql_csv_api` just like a CSV storage in MySQL to illustrate what can be done. 64 | 65 | Here is the list of functions required to be implemented: 66 | 67 | 68 | * `authorize(Username,HashedPassword,ClientSalt,Args) -> {ok, State}` `Args` here are passed from initialization, will describe below. Simplest implementation is: `<<"user">> == Username andalso sqlapi:password_hash(<<"password">>, ClientSalt) == HashedPassword` 69 | * `connect_db(Database::binary(), State) -> {ok, State1} | {error, Code, Description}`. Here and further you can reply with error and error code. Please, try to use mysql error codes. This function will connect engine to database, remember this database in your state. 70 | * `databases(State) -> [DBName::binary()]` returns list of available databases, just binary strings. 71 | * `database(State) -> undefined | DBName` return currently connected database. 72 | * `tables(State) -> [TableName::binary()]` returns list of tables in connected database. We will not allow to call it before user connects to database, you can rely on it. This is also checked for all other functions that require connection to some database. 73 | * `columns(Table::binary(),State) -> [{ColumnName::atom(),ColumnType::type()}]` Type may be: `string`, `boolean` or `integer` 74 | * `terminate(Reason,State)` maybe will be called on session closing 75 | * `select(Table::binary(),Conditions,State) -> [#{} = Row]` here you can take a look at `Conditions`. This is a complicated structure, described below 76 | * `insert(Table::binary(), [#{Key::binary() => Value}], State) -> {ok,#{status =>ok, affected_rows=>1}} | {error,Code,Desc}` will be called on inserting rows 77 | * `update(Table::binary(), #{Key::binary() => Value}, Conditions, State) -> {ok,#{status =>ok, affected_rows=>1}} | {error,Code,Desc}` will be called on updating. `Conditions` just as in `select` and only one update row is specified. 78 | * `delete(Table::binary(), Conditions, State) -> {ok,#{status =>ok, affected_rows=>1}} | {error,Code,Desc}` will be called on deleting from table. `Conditions` are used from `select` 79 | * `fncall(Name::binary(), [Param], State)` is called on SQL function call 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /ebin/.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.app 3 | 4 | -------------------------------------------------------------------------------- /example.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% 3 | %%! -pa ebin 4 | 5 | -mode(compile). 6 | 7 | 8 | main([]) -> 9 | {ok,_} = application:ensure_all_started(sqlapi), 10 | sqlapi:load_config(#{port => 4407, listener_name => test_sql, handler => sql_ets_api, trivial => true}), 11 | io:format("listening on port 4407. Connect as:\n" 12 | " mysql -h 127.0.0.1 -P 4407 -u login -ppass ets\n" 13 | "To create ets table use following syntax:\n" 14 | " SELECT create_ram_table('sessions','session_id:string','user_id:integer','last_seen_at:integer');\n" 15 | "Ensure that it is created:\n" 16 | " show tables;" 17 | "\n" 18 | "Now run some queries:\n" 19 | " insert into sessions (user_id,session_id,last_seen_at) values (1,'u1',1540123456);\n" 20 | " select * from sessions;\n" 21 | " update sessions set last_seen_at = 14123123123 where user_id=1;\n" 22 | " select * from sessions;\n" 23 | " delete from sessions where user_id=1;\n" 24 | " select * from sessions;\n" 25 | "Now try to drop it:\n" 26 | " SELECT drop_ram_table('sessions');\n" 27 | "Check that there is no 'sessions' table anymore:\n" 28 | "show tables;\n" 29 | ), 30 | receive _ -> ok end. 31 | 32 | -------------------------------------------------------------------------------- /include/sql.hrl: -------------------------------------------------------------------------------- 1 | % derived from https://github.com/altenwald/myproto under EPL: 2 | % https://github.com/altenwald/myproto/commit/d89e6cad46fa966e6149aee3ad6f0d711e6182f5 3 | -author('Manuel Rubio '). 4 | 5 | %% -*- erlang; utf-8 -*- 6 | 7 | % COMMON 8 | -record(table, {name, alias}). 9 | -record(all, {table}). 10 | -record(subquery, {name, subquery }). 11 | -record(key, {alias, name, table}). 12 | -record(value, {name, value}). 13 | -record(condition, {nexo, op1, op2}). 14 | -record(function, {name, params, alias}). 15 | -record(operation, {type, op1, op2}). 16 | -record(variable, {name, label, scope}). 17 | 18 | -record(system_set, {query}). 19 | 20 | % SHOW 21 | -record(show, {type, full, from, conditions}). 22 | 23 | -type show() :: #show{}. 24 | 25 | % SELECT 26 | -record(select, {params, tables, conditions, group, order, limit, offset}). 27 | -record(order, {key, sort}). 28 | 29 | -type select() :: #select{}. 30 | 31 | % UPDATE 32 | -record(update, {table, set, conditions}). 33 | -record(set, {key, value}). 34 | 35 | -type update() :: #update{}. 36 | 37 | % DELETE 38 | -record(delete, {table, conditions}). 39 | 40 | -type delete() :: #delete{}. 41 | 42 | % INSERT 43 | -record(insert, {table, values}). 44 | 45 | % DESCRIBE 46 | -record(describe, {table}). 47 | 48 | -type insert() :: #insert{}. 49 | 50 | -type sql() :: show() | select() | update() | delete() | insert(). 51 | -------------------------------------------------------------------------------- /include/sqlapi.hrl: -------------------------------------------------------------------------------- 1 | 2 | 3 | -record(sql_filter, { 4 | conditions, 5 | columns = [], % [] = no restrictions, use all 6 | table_columns, 7 | order, 8 | group, 9 | limit, 10 | offset 11 | }). 12 | -------------------------------------------------------------------------------- /nanomysql.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ERL_LIBS=deps escript 2 | 3 | -mode(compile). 4 | 5 | main([URL|_Args]) -> 6 | code:add_pathz("ebin"), 7 | {ok, Sock} = nanomysql:connect(URL), 8 | {ok, {mysql, _, _Host, _Port, "/"++_DBName, _}} = http_uri:parse(URL, [{scheme_defaults,[{mysql,3306}]}]), 9 | % DB = list_to_binary(DBName), 10 | % nanomysql:execute("show databases", Sock), 11 | % {ok, {_, Rows}} = nanomysql:execute("show tables", Sock), 12 | % [nanomysql:command(4, <>, Sock) || [Name] <- Rows], 13 | loop(Sock); 14 | 15 | main([]) -> 16 | io:format("~s mysql://user:password@host:port/dbname\n", [escript:script_name()]), 17 | erlang:halt(2). 18 | 19 | 20 | loop(Sock) -> 21 | case io:get_line("mysql> ") of 22 | "\\?\n" -> help(); 23 | "\\d "++Name1 -> 24 | Name = string:substr(Name1,1, length(Name1) -1), 25 | {ok, Reply} = nanomysql:command(show_fields, iolist_to_binary([Name,0]), Sock), 26 | print_reply(Reply); 27 | "exit\n" -> halt(0); 28 | "quit\n" -> halt(0); 29 | Query -> 30 | {ok, Reply} = nanomysql:execute(Query, Sock), 31 | print_reply(Reply) 32 | end, 33 | loop(Sock). 34 | 35 | 36 | help() -> 37 | io:format( 38 | "Informational:\n" 39 | % " \\d list tables\n" 40 | " \\d NAME describe table\n" 41 | % " \\l list databases\n" 42 | ). 43 | 44 | print_reply(#{affected_rows := _} = Info) -> 45 | io:format("info ~p\n", [Info]); 46 | 47 | print_reply({Columns}) -> 48 | print_columns(Columns), 49 | io:format("\nok\n"); 50 | 51 | print_reply({Columns, Rows}) -> 52 | print_columns(Columns), 53 | print_rows(Rows), 54 | io:format("\nok\n"). 55 | 56 | print_columns(Columns) -> 57 | io:format("~s\n---\n", [ string:join([io_lib:format("~s(~p)",[C,T]) || {C,T} <- Columns], ",")]). 58 | 59 | print_rows(Rows) -> 60 | [io:format("~s\n", [ string:join([io_lib:format("~p",[C]) || C <- Row],",")]) || Row <- Rows]. 61 | 62 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flussonic/sqlapi/a8f51e3cb5827366a6224585ee52024720b9c3ef/rebar -------------------------------------------------------------------------------- /src/my_datatypes.erl: -------------------------------------------------------------------------------- 1 | -module(my_datatypes). 2 | % derived from https://github.com/altenwald/myproto under EPL: 3 | % https://github.com/altenwald/myproto/commit/d89e6cad46fa966e6149aee3ad6f0d711e6182f5 4 | -author('Manuel Rubio '). 5 | 6 | -export([ 7 | string_nul_to_binary/1, 8 | binary_to_varchar/1, 9 | fix_integer_to_number/2, number_to_fix_integer/2, 10 | var_integer_to_number/1, number_to_var_integer/1, 11 | read_lenenc_string/1 12 | ]). 13 | 14 | -include("myproto.hrl"). 15 | 16 | 17 | read_lenenc_string(<<16#fc, Len:16/little, Bin:Len/binary, Rest/binary>>) -> {Bin, Rest}; 18 | read_lenenc_string(<<16#fd, Len:24/little, Bin:Len/binary, Rest/binary>>) -> {Bin, Rest}; 19 | read_lenenc_string(<<16#fe, Len:64/little, Bin:Len/binary, Rest/binary>>) -> {Bin, Rest}; 20 | read_lenenc_string(<>) -> {Bin, Rest}. 21 | 22 | 23 | %% String.NUL 24 | 25 | -spec string_nul_to_binary(String :: binary()) -> binary(). 26 | 27 | string_nul_to_binary(String) -> 28 | list_to_binary(lists:takewhile(fun(X) -> 29 | X =/= 0 30 | end, binary_to_list(String))). 31 | 32 | %% Varchars 33 | 34 | -spec binary_to_varchar(Binary::binary() | null) -> binary(). 35 | 36 | binary_to_varchar(null) -> 37 | <<16#fb>>; 38 | binary_to_varchar(Binary) -> 39 | Len = number_to_var_integer(byte_size(Binary)), 40 | <>. 41 | 42 | %% Fix Integers 43 | 44 | -spec fix_integer_to_number(Size::integer(), Data::integer()) -> integer(). 45 | 46 | fix_integer_to_number(Size, Data) when is_integer(Size) andalso is_binary(Data) -> 47 | BitSize = Size * 8, 48 | <> = Data, 49 | Num. 50 | 51 | -spec number_to_fix_integer(Size::integer(), Data::binary()) -> binary(). 52 | 53 | number_to_fix_integer(Size, Data) when is_integer(Size) andalso is_integer(Data) -> 54 | BitSize = Size * 8, 55 | <>. 56 | 57 | %% Var integers 58 | 59 | -spec var_integer_to_number(Var::binary()) -> integer(). 60 | 61 | var_integer_to_number(<<16#fc, Data:16/little>>) -> Data; 62 | var_integer_to_number(<<16#fd, Data:24/little>>) -> Data; 63 | var_integer_to_number(<<16#fe, Data:64/little>>) -> Data; 64 | var_integer_to_number(<>) -> Data. 65 | 66 | -spec number_to_var_integer(Data::integer()) -> binary(). 67 | 68 | number_to_var_integer(Data) when is_integer(Data) andalso Data < 251 -> 69 | <>; 70 | number_to_var_integer(Data) when is_integer(Data) andalso Data < 16#10000 -> 71 | <<16#fc, Data:16/little>>; 72 | number_to_var_integer(Data) when is_integer(Data) andalso Data < 16#1000000 -> 73 | <<16#fd, Data:24/little>>; 74 | number_to_var_integer(Data) when is_integer(Data) -> 75 | <<16#fe, Data:64/little>>. 76 | 77 | -------------------------------------------------------------------------------- /src/my_packet.erl: -------------------------------------------------------------------------------- 1 | -module(my_packet). 2 | % derived from https://github.com/altenwald/myproto under EPL: 3 | % https://github.com/altenwald/myproto/commit/d89e6cad46fa966e6149aee3ad6f0d711e6182f5 4 | -author('Manuel Rubio '). 5 | -author('Max Lapshin '). 6 | 7 | -export([encode/1, decode/1, decode_auth/1]). 8 | 9 | -include("myproto.hrl"). 10 | 11 | encode(#response{ 12 | status=?STATUS_EOF, id=Id, warnings=Warnings, 13 | status_flags=StatusFlags 14 | }) when Warnings == 0 andalso StatusFlags == 0 -> 15 | <<1:24/little, Id:8, ?STATUS_EOF:8>>; 16 | encode(#response{ 17 | status=?STATUS_EOF, id=Id, warnings=Warnings, 18 | status_flags=StatusFlags 19 | }) -> 20 | <<5:24/little, Id:8, ?STATUS_EOF:8, Warnings:16/little, StatusFlags:16/little>>; 21 | encode(#response{ 22 | status=?STATUS_ERR, id=Id, error_code=Error, 23 | error_info = Code, info = Info 24 | }) when Code =/= <<"">> -> 25 | Length = byte_size(Info) + 9, 26 | <>; 27 | encode(#response{ 28 | status=?STATUS_ERR, id=Id, error_code=Error, 29 | info = Info 30 | }) -> 31 | Length = byte_size(Info) + 3, 32 | <>; 33 | encode(#response{ 34 | status=?STATUS_OK, id=Id, info={Cols} 35 | }) -> 36 | %% columns 37 | {IdEof, ColsBin} = encode_column(Cols, Id), 38 | %% eof 39 | ColsEof = encode(#response{ 40 | status=?STATUS_EOF, 41 | id=IdEof, 42 | status_flags=?SERVER_STATUS_AUTOCOMMIT 43 | }), 44 | <>; 45 | encode(#response{ 46 | status=?STATUS_OK, id=Id, info={Cols, Rows} 47 | }) -> 48 | %% Column account 49 | ColLen = length(Cols), 50 | Head = <<1:24/little, Id:8, ColLen:8>>, 51 | %% columns 52 | {IdEof, ColsBin} = encode_column(Cols, Id+1), 53 | %% eof 54 | ColsEof = encode(#response{ 55 | status=?STATUS_EOF, 56 | id=IdEof, 57 | status_flags=?SERVER_STATUS_AUTOCOMMIT 58 | }), 59 | %% rows 60 | {IdEnd, RowsPack} = encode_rows(Rows, Cols, IdEof+1), 61 | %% eof 62 | RowsEof = encode(#response{ 63 | status=?STATUS_EOF, 64 | id=IdEnd, 65 | status_flags=?SERVER_STATUS_AUTOCOMMIT 66 | }), 67 | <>; 68 | encode(#response{ 69 | status=?STATUS_OK, id=Id, info = Info, 70 | affected_rows = AffectedRows, last_insert_id = LastInsertId, 71 | status_flags = StatusFlags, warnings = Warnings 72 | }) -> 73 | BinAffectedRows = my_datatypes:number_to_var_integer(AffectedRows), 74 | BinLastInsertId = my_datatypes:number_to_var_integer(LastInsertId), 75 | Length = byte_size(BinAffectedRows) + byte_size(BinLastInsertId) + byte_size(Info) + 5, 76 | << 77 | Length:24/little, Id:8, ?STATUS_OK:8, BinAffectedRows/binary, 78 | BinLastInsertId/binary, StatusFlags:16/little, Warnings:16/little, 79 | Info/binary 80 | >>; 81 | encode(#response{ 82 | status=?STATUS_HELLO, id=Id, info=Hash 83 | }) -> 84 | 20 == size(Hash) orelse error({invalid_hash_size,size(Hash),need,20}), 85 | ServerSign = case application:get_env(myproto, server_sign) of 86 | {ok, SS} when is_binary(SS) -> SS; 87 | {ok, SS} when is_list(SS) -> list_to_binary(SS); 88 | undefined -> ?SERVER_SIGN 89 | end, 90 | Caps = 91 | ?CLIENT_PLUGIN_AUTH bor %% PLAIN AUTH 92 | ?CLIENT_PROTOCOL_41 bor %% PROTOCOL 4.1 93 | ?CLIENT_SECURE_CONNECTION bor %% for mysql_native_password 94 | 0, 95 | <> = <>, 96 | % <> = Hash, 97 | <> = Hash, %<>, 98 | LenAuth = 21, 99 | StatusFlags = 100 | ?SERVER_STATUS_AUTOCOMMIT bor 101 | 0, 102 | Charset = ?UTF8_GENERAL_CI, 103 | Info = << 104 | ServerSign/binary, 0:8, Id:32/little, 105 | Auth1/binary, 0:8, CapsLow:16/little, 106 | Charset:8, StatusFlags:16/little, 107 | CapsUp:16/little, LenAuth:8, 0:80, 108 | Auth2/binary, 0:8, "mysql_native_password", 0:8 109 | >>, 110 | Length = byte_size(Info) + 1, 111 | << Length:24/little, 0:8, ?STATUS_HELLO:8, Info/binary >>. 112 | 113 | encode_column(Cols, Id) when is_list(Cols) -> 114 | lists:foldl(fun(Col, {NewId, Data}) -> 115 | BinCol = encode_column(Col, NewId), 116 | {NewId+1, <>} 117 | end, {Id, <<"">>}, Cols); 118 | encode_column(#column{ 119 | schema = Schema, table = Table, name = Name, 120 | charset = Charset, length = L, type = Type, 121 | flags = Flags, decimals = Decimals, org_name = ON 122 | }=_Column, Id) when is_binary(Schema), is_binary(Table), is_binary(Name), 123 | is_integer(Charset), is_integer(Type), is_integer(Flags), 124 | is_integer(Decimals) -> 125 | SchemaLen = my_datatypes:number_to_var_integer(byte_size(Schema)), 126 | TableLen = my_datatypes:number_to_var_integer(byte_size(Table)), 127 | NameLen = my_datatypes:number_to_var_integer(byte_size(Name)), 128 | {OrgNameLen, OrgName} = case ON of 129 | undefined -> {NameLen, bin_to_upper(Name)}; 130 | ON -> {my_datatypes:number_to_var_integer(byte_size(ON)), ON} 131 | end, 132 | Length = case {Type, L} of 133 | _ when is_integer(L) -> L; 134 | {?TYPE_DATETIME,undefined} -> 16#13; 135 | {?TYPE_LONGLONG,undefined} -> 16#15; 136 | {_,undefined} -> 0 137 | end, 138 | Payload = << 139 | 3:8, "def", 140 | SchemaLen/binary, Schema/binary, 141 | TableLen/binary, Table/binary, % table 142 | TableLen/binary, Table/binary, % org_table 143 | NameLen/binary, Name/binary, % name 144 | OrgNameLen/binary, OrgName/binary, % org_name 145 | 16#0c:8, Charset:16/little, 146 | Length:32/little, Type:8, Flags:16/little, 147 | Decimals:8/little, 148 | 0:16/little 149 | >>, 150 | PayloadLen = byte_size(Payload), 151 | <>. 152 | 153 | encode_rows(Rows, Cols, Id) -> 154 | lists:foldl(fun(Values, {NewId, Data}) -> 155 | Payload = lists:foldl(fun({#column{type = Type, name = Name}, Cell}, Binary) -> 156 | Cell1 = case Type of 157 | _ when Cell == undefined -> undefined; 158 | _ when Cell == null -> undefined; 159 | _ when Cell == true -> <<"1">>; 160 | _ when Cell == false -> <<"0">>; 161 | T when (T == ?TYPE_TINY orelse 162 | T == ?TYPE_SHORT orelse 163 | T == ?TYPE_LONG orelse 164 | T == ?TYPE_LONGLONG orelse 165 | T == ?TYPE_INT24 orelse 166 | T == ?TYPE_YEAR) andalso is_integer(Cell) -> integer_to_binary(Cell); 167 | _ when is_binary(Cell) -> Cell; 168 | _ when is_float(Cell) -> integer_to_binary(round(Cell)); 169 | _ when is_atom(Cell) -> atom_to_binary(Cell,latin1); 170 | _ -> error({cannot_encode,Name,Type,Cell}) 171 | end, 172 | CellEnc = case Cell of 173 | undefined -> ?DATA_NULL; 174 | null -> ?DATA_NULL; 175 | _ -> my_datatypes:binary_to_varchar(Cell1) 176 | end, 177 | <> 178 | end, <<"">>, lists:zip(Cols,Values)), 179 | Length = byte_size(Payload), 180 | {NewId+1, <>} 181 | end, {Id, <<"">>}, Rows). 182 | 183 | 184 | 185 | -spec decode_auth(binary()) -> {ok, user(), binary()} | {more, binary()}. 186 | 187 | 188 | decode_auth(<>) -> 189 | {ok, decode_auth0(Bin), Rest}; 190 | 191 | decode_auth(<>) -> 192 | {more, size(Bin) - Length}; 193 | 194 | decode_auth(<>) -> 195 | {more, 4 - size(Bin)}. 196 | 197 | 198 | decode_auth0(<>) -> 199 | Caps = unpack_caps(CapsFlag), 200 | {User, Info1} = unpack_zero(Info0), 201 | 202 | {Password,Info2} = case proplists:get_value(auth_lenenc_client_data,Caps) of 203 | true -> my_datatypes:read_lenenc_string(Info1); 204 | _ -> 205 | case proplists:get_value(secure_connection, Caps) of 206 | true -> 207 | <> = Info1, 208 | {Pass, R}; 209 | false -> 210 | unpack_zero(Info1) 211 | end 212 | end, 213 | 214 | HasPluginAuth = proplists:get_value(plugin_auth, Caps), 215 | {DB, Info3} = case proplists:get_value(connect_with_db, Caps) of 216 | % For some strange reasons mysql 5.0.6 violates protocol and doesn't send db name 217 | % http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse41 218 | % so we write here a dirty hack for pymysql 219 | true when HasPluginAuth == undefined andalso size(Info2) > 0 -> 220 | unpack_zero(Info2); 221 | _ -> 222 | {undefined, Info2} 223 | end, 224 | {Plugin, _Info4} = case proplists:get_value(plugin_auth, Caps) of 225 | true -> 226 | unpack_zero(Info3); 227 | _ -> 228 | {undefined, Info3} 229 | end, 230 | UserData = #user{ 231 | name=User, 232 | password=Password, 233 | plugin=Plugin, 234 | capabilities=Caps, 235 | database=DB, 236 | charset=Charset 237 | }, 238 | #request{command=?COM_AUTH, info=UserData}. 239 | 240 | unpack_zero(String) -> 241 | [B1, B2] = binary:split(String, <<0>>), 242 | {B1, B2}. 243 | 244 | 245 | unpack_caps(Flag) -> 246 | Caps = [ 247 | {?CLIENT_LONG_PASSWORD,long_password}, 248 | {?CLIENT_FOUND_ROWS,found_rows}, 249 | {?CLIENT_LONG_FLAG, long_flag}, 250 | {?CLIENT_CONNECT_WITH_DB, connect_with_db}, 251 | {?CLIENT_NO_SCHEMA, no_schema}, 252 | {?CLIENT_COMPRESS, compress}, 253 | {?CLIENT_ODBC, odbc}, 254 | {?CLIENT_LOCAL_FILES, local_files}, 255 | {?CLIENT_IGNORE_SPACE, ignore_space}, 256 | {?CLIENT_PROTOCOL_41, protocol_41}, 257 | {?CLIENT_INTERACTIVE, interactive}, 258 | {?CLIENT_SSL, ssl}, 259 | {?CLIENT_IGNORE_SIGPIPE, ignore_sigpipe}, 260 | {?CLIENT_TRANSACTIONS, transactions}, 261 | {?CLIENT_SECURE_CONNECTION, secure_connection}, 262 | {?CLIENT_MULTI_STATEMENTS, multi_statements}, 263 | {?CLIENT_MULTI_RESULTS, multi_results}, 264 | {?CLIENT_PS_MULTI_RESULTS, ps_multi_results}, 265 | {?CLIENT_PLUGIN_AUTH, plugin_auth}, 266 | {?CLIENT_CONNECT_ATTRS, connect_attrs}, 267 | {?CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA, auth_lenenc_client_data} 268 | ], 269 | lists:flatmap(fun({I,Tag}) -> 270 | case Flag band I of 271 | 0 -> []; 272 | _ -> [Tag] 273 | end 274 | end, Caps). 275 | 276 | 277 | -spec decode(binary()) -> {ok, response(), binary()} | {more, binary()}. 278 | 279 | decode(<>) -> 280 | {ok, decode0(Length, Id, Bin), Rest}; 281 | 282 | decode(<>) -> 283 | {more, Length - size(Bin)}; 284 | 285 | decode(<>) -> 286 | {more, 4 - size(Bin)}. 287 | 288 | decode0(_, Id, <>) -> 289 | #request{ 290 | command=?COM_FIELD_LIST, 291 | id=Id, 292 | info=my_datatypes:string_nul_to_binary(Info) 293 | }; 294 | decode0(16#ffffff, Id, <>) -> 295 | #request{ 296 | command=Command, 297 | id=Id, 298 | info=Info, 299 | continue=true 300 | }; 301 | decode0(_, Id, <>) -> 302 | #request{ 303 | command=Command, 304 | id=Id, 305 | info=Info, 306 | continue=false 307 | }. 308 | 309 | bin_to_upper(Lower) when is_binary(Lower) -> 310 | << <<( 311 | if 312 | X >= $a andalso X =< $z -> X-32; 313 | true -> X end 314 | )/integer>> || <> <= Lower >>. 315 | -------------------------------------------------------------------------------- /src/my_protocol.erl: -------------------------------------------------------------------------------- 1 | % @doc 2 | % This is a server-side protocol implementation 3 | % 4 | % When server process receives socket, it MUST first call hello: 5 | % 6 | % handle_info({socket, Socket}, State) -> 7 | % {ok, Hello, My} = my_protocol:hello(ConnectionId), % here ConnectionId is a thread id 8 | % ok = gen_tcp:send(Socket, Hello), 9 | % inet:setopts(Socket, [{active,once}]), 10 | % {noreply, State#state{my = My}}; 11 | % 12 | -module(my_protocol). 13 | -include("myproto.hrl"). 14 | % MIT License 15 | -author('Max Lapshin '). 16 | 17 | -export([init/0, init/1]). 18 | -export([decode/2, decode/1, buffer_bytes/2]). 19 | -export([send_or_reply/2, hello/2, ok/1, error/2, error/3]). 20 | -export([next_packet/1]). 21 | -export([hash_password/2]). 22 | 23 | 24 | -record(my, { 25 | connection_id :: non_neg_integer(), %% connection id 26 | hash ::binary(), %% hash for auth 27 | state :: auth | normal, 28 | parse_query = true :: boolean(), %% parse query or not 29 | buffer :: undefined | binary(), 30 | query_buffer = <<>> :: binary(), %% buffer for long queries 31 | socket :: undefined | inet:socket(), %% When socket is set, client will send data 32 | id = 1 :: non_neg_integer() 33 | }). 34 | 35 | -type my() :: #my{}. 36 | 37 | 38 | init() -> 39 | init([]). 40 | 41 | init(Options) -> 42 | Socket = proplists:get_value(socket, Options), 43 | ParseQuery = proplists:get_value(parse_query, Options, true), 44 | #my{socket = Socket, parse_query = ParseQuery}. 45 | 46 | 47 | -spec hello(ConnectionId::non_neg_integer(), my()) -> {ok, Bin::iodata(), State::my()} | {ok, my()}. 48 | 49 | hello(ConnectionId, #my{} = My) when is_integer(ConnectionId) -> 50 | Hash = list_to_binary( 51 | lists:map(fun 52 | (0) -> 1; 53 | (X) -> X 54 | end, binary_to_list( 55 | crypto:strong_rand_bytes(20) 56 | )) 57 | ), 58 | Hello = #response{ 59 | id=ConnectionId, 60 | status=?STATUS_HELLO, 61 | info=Hash 62 | }, 63 | send_or_reply(Hello, My#my{connection_id = ConnectionId, hash = Hash, state = auth, id = 2}). 64 | 65 | 66 | 67 | 68 | send_or_reply(ok, #my{} = My) -> 69 | ok(My); 70 | 71 | send_or_reply(#response{id = 0} = Response, #my{id = Id} = My) -> 72 | send_or_reply(Response#response{id = Id}, My#my{id = Id+1}); 73 | 74 | send_or_reply(#response{} = Response, #my{socket = Socket} = My) -> 75 | ok = gen_tcp:send(Socket, my_packet:encode(Response)), 76 | {ok, My}. 77 | 78 | 79 | 80 | 81 | -spec ok(my()) -> {ok, Reply::binary(), my()} | {ok, my()}. 82 | 83 | ok(#my{} = My) -> 84 | Response = #response{ 85 | status = ?STATUS_OK, 86 | status_flags = ?SERVER_STATUS_AUTOCOMMIT 87 | }, 88 | send_or_reply(Response, My). 89 | 90 | 91 | 92 | -spec error(Reason::binary(), my()) -> {ok, Reply::binary(), my()} | {ok, my()}. 93 | 94 | error(Reason, #my{} = My) when is_binary(Reason) -> 95 | error(1045, Reason, My). 96 | 97 | 98 | -spec error(Code::non_neg_integer(), Reason::binary(), my()) -> {ok, Reply::binary(), my()} | {ok, my()}. 99 | 100 | error(Code, Reason, #my{} = My) when is_integer(Code), is_binary(Reason) -> 101 | Response = #response{ 102 | status = ?STATUS_ERR, 103 | error_code = Code, 104 | info = Reason 105 | }, 106 | send_or_reply(Response, My). 107 | 108 | 109 | 110 | 111 | 112 | -spec decode(binary(), my()) -> {ok, Reply::request(), my()}. 113 | 114 | decode(Bin, #my{} = My) -> 115 | My1 = buffer_bytes(Bin, My), 116 | decode(My1). 117 | 118 | 119 | -spec buffer_bytes(binary(), my()) -> my(). 120 | 121 | buffer_bytes(Bin, #my{buffer = Buffer} = My) when size(Buffer) > 0 andalso size(Bin) > 0 -> 122 | My#my{buffer = <>}; 123 | 124 | buffer_bytes(Bin, #my{} = My) -> 125 | My#my{buffer = Bin}. 126 | 127 | 128 | 129 | -spec decode(my()) -> {ok, Reply::request(), my()}. 130 | decode(#my{buffer = Bin, state = auth, hash = Hash} = My) when size(Bin) > 4 -> 131 | case my_packet:decode_auth(Bin) of 132 | {more, _} -> 133 | {more, My}; 134 | {ok, #request{info = #user{} = User} = Req, Rest} -> 135 | {ok, Req#request{info = User#user{server_hash = Hash}}, My#my{state = normal, buffer = Rest}} 136 | end; 137 | 138 | decode(#my{buffer = Bin, state = normal, parse_query = ParseQuery, query_buffer = QB} = My) when size(Bin) > 4 -> 139 | case my_packet:decode(Bin) of 140 | {more, _} -> 141 | {more, My}; 142 | {ok, #request{continue = true, info = Info}, Rest} -> 143 | QB1 = case QB of 144 | <<>> -> Info; 145 | _ -> <> 146 | end, 147 | decode(My#my{buffer = Rest, query_buffer = QB1}); 148 | {ok, #request{continue = false, info = Info, command = CommandCode, id = Id} = Packet, Rest} -> 149 | Query = case QB of 150 | <<>> -> Info; 151 | _ -> <> 152 | end, 153 | Command = case CommandCode of 154 | ?COM_SLEEP -> sleep; 155 | ?COM_QUIT -> quit; 156 | ?COM_INIT_DB -> init_db; 157 | ?COM_QUERY -> 'query'; 158 | ?COM_FIELD_LIST -> field_list; 159 | ?COM_CREATE_DB -> create_db; 160 | ?COM_DROP_DB -> drop_db; 161 | ?COM_REFRESH -> refresh; 162 | ?COM_SHUTDOWN -> shutdown; 163 | ?COM_STATISTICS -> statistics; 164 | ?COM_PROCESS_INFO -> process_info; 165 | ?COM_CONNECT -> connect; 166 | ?COM_PROCESS_KILL -> process_kill; 167 | ?COM_DEBUG -> debug; 168 | ?COM_PING -> ping; 169 | Else -> Else 170 | end, 171 | 172 | Packet1 = case Command of 173 | 'query' when ParseQuery -> 174 | S = size(Query) - 1, 175 | Query2 = case Query of 176 | <> -> Query1; 177 | _ -> Query 178 | end, 179 | % T1 = os:system_time(micro_seconds), 180 | SQL = case sql92:parse(Query2) of 181 | {fail,Expected} -> {parse_error, {fail,Expected}, Info}; 182 | % {_, Extra,Where} -> {parse_error, {Extra, Where}, Info}; 183 | 184 | Parsed -> Parsed 185 | end, 186 | Packet#request{command = Command, info = SQL, text = Query2}; 187 | _ -> 188 | Packet#request{command = Command, info = Query} 189 | end, 190 | {ok, Packet1, My#my{buffer = Rest, id = Id + 1}} 191 | end; 192 | 193 | decode(#my{} = My) -> 194 | {more, My}. 195 | 196 | 197 | 198 | 199 | 200 | 201 | next_packet(#my{buffer = Buffer, socket = Socket} = My) when Socket =/= undefined andalso (Buffer == undefined orelse Buffer == <<>>) -> 202 | case gen_tcp:recv(Socket, 4) of 203 | {ok, <> = Header} -> 204 | case gen_tcp:recv(Socket, Length) of 205 | {ok, Bin} when size(Bin) == Length -> 206 | case decode(<
>, My) of 207 | {more, My1} -> next_packet(My1); 208 | {ok, Response, My1} -> {ok, Response, My1} 209 | end; 210 | {error, _} = Error -> 211 | Error 212 | end; 213 | {error, _} = Error -> 214 | Error 215 | end. 216 | 217 | 218 | 219 | hash_password(Password, Salt) -> 220 | Hash = crypto:hash(sha, Password), 221 | Res = crypto:hash_final( 222 | crypto:hash_update( 223 | crypto:hash_update(crypto:hash_init(sha), Salt), 224 | crypto:hash(sha, Hash) 225 | ) 226 | ), 227 | crypto:exor(Hash, Res). 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /src/my_ranch_worker.erl: -------------------------------------------------------------------------------- 1 | -module(my_ranch_worker). 2 | % MIT License 3 | -author('Max Lapshin '). 4 | 5 | -export([start_server/4, stop_server/1, start_link/4, init_server/4]). 6 | 7 | -export([start_trivial_server/4, init_trivial_listener/4, init_trivial_server/2]). 8 | 9 | -export([handle_call/3, handle_info/2, terminate/2]). 10 | -include("myproto.hrl"). 11 | 12 | 13 | start_server(Port, Name, Handler, Args) -> 14 | application:start(ranch), 15 | ranch:start_listener(Name, 10, ranch_tcp, [{port, Port},{backlog,4096},{max_connections,32768}], ?MODULE, [Handler, Args]). 16 | 17 | stop_server(Name) -> 18 | ranch:stop_listener(Name). 19 | 20 | 21 | start_link(ListenerPid, Socket, _Transport, [Handler, Args]) -> 22 | proc_lib:start_link(?MODULE, init_server, [ListenerPid, Socket, Handler, Args]). 23 | 24 | 25 | 26 | % Do not use this for anything except tests or debug. 27 | start_trivial_server(Port, Name, Handler, Args) -> 28 | proc_lib:start(?MODULE, init_trivial_listener, [Port, Name, Handler, Args]). 29 | 30 | init_trivial_listener(Port, Name, Handler, Args) -> 31 | {ok, LSocket} = gen_tcp:listen(Port,[binary, {reuseaddr, true}]), 32 | register(Name, self()), 33 | {ok, ListenPort} = inet:port(LSocket), 34 | put('$my_listener_port', ListenPort), 35 | proc_lib:init_ack({ok, self()}), 36 | loop_trivial_listener(LSocket, Handler, Args). 37 | 38 | loop_trivial_listener(LSocket, Handler, Args) -> 39 | {ok, Socket} = gen_tcp:accept(LSocket), 40 | inet:setopts(Socket, [binary, {active,false}, {packet,raw}]), 41 | case proc_lib:start(?MODULE, init_trivial_server, [Handler, Args]) of 42 | {ok, Pid} -> 43 | gen_tcp:controlling_process(Socket, Pid), 44 | Pid ! {run,Socket}; 45 | _Else -> 46 | ok 47 | end, 48 | loop_trivial_listener(LSocket, Handler, Args). 49 | 50 | 51 | 52 | 53 | -record(server, { 54 | handler, 55 | args, 56 | state, 57 | database, 58 | socket, 59 | my 60 | }). 61 | 62 | init_server(ListenerPid, Socket, Handler, Args) -> 63 | proc_lib:init_ack({ok, self()}), 64 | ranch:accept_ack(ListenerPid), 65 | init_server3(Socket, Handler, Args). 66 | 67 | init_trivial_server(Handler, Args) -> 68 | proc_lib:init_ack({ok, self()}), 69 | receive 70 | {run, Socket} -> init_server3(Socket, Handler, Args) 71 | after 72 | 5000 -> exit(not_activated) 73 | end. 74 | 75 | 76 | init_server3(Socket, Handler, Args) -> 77 | My0 = my_protocol:init([{socket, Socket},{parse_query,true}]), 78 | {ok, My1} = my_protocol:hello(42, My0), 79 | case my_protocol:next_packet(My1) of 80 | {ok, #request{info = #user{} = User}, My2} -> 81 | try authorize_and_connect(User, Handler, Args) of 82 | {ok, HandlerState, DB} -> 83 | {ok, My3} = my_protocol:ok(My2), 84 | inet:setopts(Socket, [{active,once}]), 85 | State = #server{handler = Handler, state = HandlerState, socket = Socket, my = My3, database = DB}, 86 | catch gen_server:enter_loop(?MODULE, [], State); 87 | {error, Reason} -> 88 | my_protocol:error(1045, Reason, My2); 89 | {error, Code, Reason} -> 90 | my_protocol:error(Code, Reason, My2) 91 | catch 92 | _C:E -> 93 | ST = erlang:get_stacktrace(), 94 | sqlapi:notify(sql_error, [{module,?MODULE},{line,?LINE},{pid,self()},{application,sqlapi}, 95 | {query, <<"login">>}, {error, E}, {stacktrace, iolist_to_binary(io_lib:format("~p",[ST]))}]), 96 | my_protocol:error(500, <<"Internal server error on auth">>, My2) 97 | end; 98 | {error, StartError} -> 99 | {stop, StartError} 100 | end. 101 | 102 | authorize_and_connect(#user{} = User, Handler, Args) -> 103 | #user{name = Name, password = Password, server_hash = Hash, database = Database} = User, 104 | case Handler:authorize(Name, Password, Hash, Args) of 105 | {ok, HandlerState} when size(Database) > 0 -> 106 | case Handler:connect_db(Database, HandlerState) of 107 | {ok, HandlerState1} -> {ok, HandlerState1, Database}; 108 | {error, Code, E} -> {error, Code, E}; 109 | {error, E} -> {error, E} 110 | end; 111 | {ok, HandlerState} -> 112 | {ok, HandlerState, undefined}; 113 | {error, Code, E} -> 114 | {error, Code, E}; 115 | {error, E} -> 116 | {error, E} 117 | end. 118 | 119 | 120 | 121 | handle_call(Call, _From, #server{} = Server) -> 122 | {stop, {unknown_call,Call}, Server}. 123 | 124 | 125 | handle_info({tcp, Socket, Bin}, #server{my = My} = Server) -> 126 | My1 = my_protocol:buffer_bytes(Bin, My), 127 | inet:setopts(Socket, [{active, once}]), 128 | try handle_packets(Server#server{my = My1}) 129 | catch 130 | Class:Error -> 131 | ST = erlang:get_stacktrace(), 132 | sqlapi:notify(sql_handler_error, [{module,?MODULE},{line,?LINE},{pid,self()},{application,sqlapi}, 133 | {error,iolist_to_binary(io_lib:format("~p",[Error]))}, 134 | {stacktrace,iolist_to_binary(io_lib:format("~p",[ST]))}]), 135 | {stop, {Class,Error}, Server} 136 | end; 137 | 138 | handle_info({tcp_closed, _Socket}, #server{handler = Handler, state = HandlerState} = Server) -> 139 | Handler:terminate(tcp_closed, HandlerState), 140 | {stop, normal, Server}; 141 | 142 | handle_info({tcp_error, _, Error}, #server{handler = Handler, state = HandlerState} = Server) -> 143 | Handler:terminate({tcp_error, Error}, HandlerState), 144 | {stop, normal, Server}. 145 | 146 | 147 | terminate(_,_) -> ok. 148 | 149 | 150 | 151 | handle_packets(#server{my = My} = Server) -> 152 | case my_protocol:decode(My) of 153 | {ok, Query, My1} -> 154 | case handle_packet(Query, Server#server{my = My1}) of 155 | {ok, Server1} -> 156 | handle_packets(Server1); 157 | {stop, Reason, Server1} -> 158 | {stop, Reason, Server1} 159 | end; 160 | {more, My1} -> 161 | {noreply, Server#server{my = My1}} 162 | end. 163 | 164 | 165 | 166 | 167 | 168 | handle_packet(#request{command=quit}, #server{handler=H,state=S} = Server) -> 169 | S1 = H:terminate(quit, S), 170 | {stop, normal, Server#server{state=S1}}; 171 | 172 | handle_packet(#request{} = Request, #server{handler=H,state=S,my=My1,database=DB} = Server) -> 173 | put('$current_request',Request), 174 | case default_reply(Request, Server) of 175 | {reply, Reply, Server1} -> 176 | {ok, My2} = my_protocol:send_or_reply(Reply, My1), 177 | erase('$current_request'), 178 | {ok, Server1#server{my=My2}}; 179 | undefined when DB == undefined -> 180 | Reply = #response{status=?STATUS_ERR, error_code=1046, info = <<"No database selected">>}, 181 | {ok, My2} = my_protocol:send_or_reply(Reply, My1), 182 | erase('$current_request'), 183 | {ok, Server#server{my=My2}}; 184 | undefined -> 185 | {reply, Reply, S1} = sqlapi:execute(Request,H,S), 186 | {ok, My2} = my_protocol:send_or_reply(Reply, My1), 187 | erase('$current_request'), 188 | {ok, Server#server{state=S1,my=My2}} 189 | end. 190 | 191 | 192 | 193 | % This is a hack for Flask 194 | default_reply(#request{command = 'query', text = <<"SELECT CAST(", _/binary>> = Text}, State) -> 195 | case re:run(Text, "CAST\\('([^']+)' AS (\\w+)", [{capture,all_but_first,binary}]) of 196 | {match, [Value, Name]} -> 197 | Info = { 198 | [#column{name = Name, type=?TYPE_VARCHAR, length=200}], 199 | [[Value]] 200 | }, 201 | {reply, #response{status=?STATUS_OK, info=Info}, State}; 202 | nomatch -> 203 | {reply, #response{status=?STATUS_ERR, error_code=1065, info = <<"Unsupported CAST">>}, State} 204 | end; 205 | 206 | 207 | default_reply(#request{info = #select{params=[#variable{name = <<"version_comment">>}]}}, #server{}=Srv) -> 208 | Version = version_from_handler(Srv), 209 | Info = { 210 | [#column{name = <<"@@version_comment">>, type=?TYPE_VARCHAR, length=20}], 211 | [[Version]] 212 | }, 213 | {reply, #response{status=?STATUS_OK, info=Info}, Srv}; 214 | 215 | default_reply(#request{info = #select{params=[#variable{name = <<"global.max_allowed_packet">>}]}}, State) -> 216 | Info = { 217 | [#column{name = <<"@@global.max_allowed_packet">>, type=?TYPE_LONG, length=20}], 218 | [[4194304]] 219 | }, 220 | {reply, #response{status=?STATUS_OK, info=Info}, State}; 221 | 222 | default_reply(#request{info = #select{params=[#variable{name = <<"tx_isolation">>, scope = local}]}}, State) -> 223 | Info = { 224 | [#column{name = <<"@@tx_isolation">>, type=?TYPE_VARCHAR}], 225 | [[<<"REPEATABLE-READ">>]] 226 | }, 227 | {reply, #response{status=?STATUS_OK, info=Info}, State}; 228 | 229 | 230 | default_reply(#request{info = {use,Database}}, #server{handler=Handler,state=S}=Srv) -> 231 | case Handler:connect_db(Database, S) of 232 | {ok, S1} -> 233 | {reply, #response { 234 | status=?STATUS_OK, info = <<"Changed to ", Database/binary>>, status_flags = 2 235 | }, Srv#server{state=S1,database=Database}}; 236 | {error, Code, E} -> 237 | {reply, #response{status=?STATUS_ERR, error_code = Code, info = E}, Srv} 238 | end; 239 | 240 | default_reply(#request{command = init_db, info = Database}, Srv) -> 241 | default_reply(#request{info={use,Database}}, Srv); 242 | 243 | default_reply(#request{info = #select{params = [#function{name = <<"DATABASE">>}]}}, #server{handler=H,state=S}=Srv) -> 244 | case H:database(S) of 245 | undefined -> 246 | {reply, #response{status=?STATUS_OK, info = { 247 | [#column{name = <<"database()">>, type = ?TYPE_NULL}], 248 | [[null]] 249 | }}, Srv}; 250 | Database -> 251 | {reply, #response{status=?STATUS_OK, info = { 252 | [#column{name = <<"database()">>, type = ?TYPE_VAR_STRING, length = 20}], 253 | [[Database]] 254 | }}, Srv} 255 | end; 256 | 257 | 258 | default_reply(#request{info = #select{tables = [#table{name = {<<"information_schema">>,<<"tables">>}}]}}, Server) -> 259 | default_reply(#request{info = #show{type = tables}}, Server); 260 | 261 | default_reply(#request{info = #select{tables = [#table{name = {<<"information_schema">>,_}}]} = Select}, State) -> 262 | #select{params = Params} = Select, 263 | ReplyColumns = [#column{name = or_(Alias,Name), type = ?TYPE_VAR_STRING} || #key{name=Name,alias=Alias} <- Params], 264 | Response = {ReplyColumns, []}, 265 | {reply, #response{status=?STATUS_OK, info = Response}, State}; 266 | 267 | 268 | 269 | default_reply(#request{info = #show{type = databases}}, #server{handler=Handler,state=S}=Srv) -> 270 | Databases = Handler:databases(S), 271 | ResponseFields = { 272 | [#column{name = <<"Database">>, type=?TYPE_VAR_STRING, length=20, schema = <<"information_schema">>, table = <<"SCHEMATA">>, org_table = <<"SCHEMATA">>, org_name = <<"SCHEMA_NAME">>}], 273 | [ [DB] || DB <- Databases] 274 | }, 275 | Response = #response{status=?STATUS_OK, info = ResponseFields}, 276 | {reply, Response, Srv}; 277 | 278 | 279 | default_reply(#request{info = #show{type = collation}}, State) -> 280 | ResponseFields = { 281 | [#column{name = <<"Collation">>, type=?TYPE_VAR_STRING, length=20}, 282 | #column{name = <<"Charset">>, type=?TYPE_VAR_STRING, length=20}, 283 | #column{name = <<"Id">>, type=?TYPE_LONG}, 284 | #column{name = <<"Default">>, type=?TYPE_VAR_STRING, length=20}, 285 | #column{name = <<"Compiled">>, type=?TYPE_VAR_STRING, length=20}, 286 | #column{name = <<"Sortlen">>, type=?TYPE_LONG} 287 | ], 288 | [ 289 | [<<"utf8_bin">>,<<"utf8">>,83,<<"">>,<<"Yes">>,1] 290 | ] 291 | }, 292 | {reply, #response{status=?STATUS_OK, info = ResponseFields}, State}; 293 | 294 | 295 | 296 | default_reply(#request{info = #show{type = variables}}, #server{}=Srv) -> 297 | Version = version_from_handler(Srv), 298 | Timestamp = integer_to_binary(erlang:system_time(micro_seconds)), 299 | 300 | Variables = [ 301 | {<<"sql_mode">>, <<"NO_ENGINE_SUBSTITUTION">>}, 302 | {<<"auto_increment_increment">>, <<"1">>}, 303 | {<<"character_set_client">>, <<"utf8">>}, 304 | {<<"character_set_connection">>, <<"utf8">>}, 305 | {<<"character_set_database">>, <<"utf8">>}, 306 | {<<"character_set_results">>, <<"utf8">>}, 307 | {<<"character_set_server">>, <<"utf8">>}, 308 | {<<"character_set_system">>, <<"utf8">>}, 309 | {<<"date_format">>, <<"%Y-%m-%d">>}, 310 | {<<"datetime_format">>, <<"%Y-%m-%d %H:%i:%s">>}, 311 | {<<"default_storage_engine">>, <<"MyISAM">>}, 312 | {<<"timestamp">>, Timestamp}, 313 | {<<"version">>, Version} 314 | ], 315 | 316 | ResponseFields = { 317 | 318 | [#column{name = <<"Variable_name">>, type=?TYPE_VAR_STRING, length=20, schema = <<"information_schema">>, table = <<"SCHEMATA">>, org_table = <<"SCHEMATA">>, org_name = <<"SCHEMA_NAME">>}, 319 | #column{name = <<"Value">>, type=?TYPE_VAR_STRING, length=20, schema = <<"information_schema">>, table = <<"SCHEMATA">>, org_table = <<"SCHEMATA">>, org_name = <<"SCHEMA_NAME">>}], 320 | 321 | [tuple_to_list(V) || V <- Variables] 322 | 323 | }, 324 | {reply, #response{status=?STATUS_OK, info = ResponseFields}, Srv}; 325 | 326 | 327 | default_reply(#request{info = #select{params = [#function{name = <<"DATABASE">>}]}}, State) -> 328 | ResponseFields = { 329 | [#column{name = <<"DATABASE()">>, type=?TYPE_VAR_STRING, length=102, flags = 0, decimals = 31}], 330 | [] 331 | }, 332 | Response = #response{status=?STATUS_OK, info = ResponseFields}, 333 | {reply, Response, State}; 334 | 335 | 336 | default_reply(#request{info = #show{type = tables}}, #server{database=undefined}=Srv) -> 337 | {reply, #response{status=?STATUS_ERR, error_code=1046, info = <<"No database selected">>}, Srv}; 338 | 339 | default_reply(#request{info = #show{type = tables}}, #server{handler=Handler,state=S}=Srv) -> 340 | Tables = Handler:tables(S), 341 | DB = Handler:database(S), 342 | ResponseFields = { 343 | [#column{name = <<"Tables_in_", DB/binary>>, type=?TYPE_VAR_STRING, schema = DB, table = <<"TABLE_NAMES">>, 344 | org_table = <<"TABLE_NAMES">>, org_name = <<"TABLE_NAME">>, flags = 256, length = 192, decimals = 0}], 345 | [ [Table] || Table <- Tables] 346 | }, 347 | Response = #response{status=?STATUS_OK, info = ResponseFields}, 348 | {reply, Response, Srv}; 349 | 350 | default_reply(#request{info = #describe{table = #table{name = Table}}}, Server) -> 351 | default_reply(#request{info = #show{type = fields, from = Table, full = false }}, Server); 352 | 353 | default_reply(#request{info = #show{type = fields}}, #server{database=undefined}=Srv) -> 354 | {reply, #response{status=?STATUS_ERR, error_code=1046, info = <<"No database selected">>}, Srv}; 355 | 356 | default_reply(#request{info = #show{type = fields, from = Table, full = Full}}, #server{handler=Handler,state=S}=Srv) -> 357 | Fields = Handler:columns(Table, S), 358 | Header = [ 359 | #column{name = <<"Field">>, org_name = <<"COLUMN_NAME">>, type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 192, flags = 256}, 360 | #column{name = <<"Type">>, org_name = <<"COLUMN_TYPE">>, type = ?TYPE_BLOB, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 589815, flags = 256}, 361 | #column{name = <<"Null">>, org_name = <<"IS_NULLABLE">>, type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 9, flags = 256}, 362 | #column{name = <<"Key">>, org_name = <<"COLUMN_KEY">>, type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 9, flags = 256}, 363 | #column{name = <<"Default">>, org_name = <<"COLUMN_DEFAULT">>, type = ?TYPE_BLOB, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 589815, flags = 256}, 364 | #column{name = <<"Extra">>, org_name = <<"EXTRA">>, type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 90, flags = 256} 365 | ] ++ case Full of 366 | true -> [ 367 | #column{name = <<"Collation">>, org_name = <<"COLLATION_NAME">>, type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 96, flags = 256}, 368 | #column{name = <<"Privileges">>, org_name = <<"PRIVILEGES">>, type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 240, flags = 256}, 369 | #column{name = <<"Comment">>, org_name = <<"COLUMN_COMMENT">>, type = ?TYPE_VAR_STRING, table = <<"COLUMNS">>, org_table = <<"COLUMNS">>, schema = <<"information_schema">>, length = 3072, flags = 256} 370 | ]; 371 | false -> [] 372 | end, 373 | Rows = lists:map(fun({Name,Type}) -> 374 | [atom_to_binary(Name,latin1), 375 | case Type of string -> <<"varchar(255)">>; boolean -> <<"tinyint(1)">>; _ -> <<"bigint(20)">> end, 376 | <<"YES">>, 377 | <<>>, 378 | undefined, 379 | <<>> 380 | ] ++ case Full of 381 | true -> [case Type of string -> <<"utf8_general_ci">>; _ -> undefined end, <<"select,insert,update,references">>,<<>>]; 382 | false -> [] 383 | end 384 | end, Fields), 385 | {reply, #response{status=?STATUS_OK, info = {Header, Rows}}, Srv}; 386 | 387 | 388 | 389 | 390 | default_reply(#request{info = #show{type = index}}, #server{database=undefined}=Srv) -> 391 | {reply, #response{status=?STATUS_ERR, error_code=1046, info = <<"No database selected">>}, Srv}; 392 | 393 | 394 | default_reply(#request{info = #show{type = index}}, #server{}=Srv) -> 395 | Header = [ 396 | #column{name = <<"Table">>, org_name = <<"Table">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 192, flags = 256}, 397 | #column{name = <<"Non_unique">>, org_name = <<"Non_unique">>, type = ?TYPE_LONGLONG, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 1, flags = 256}, 398 | #column{name = <<"Key_name">>, org_name = <<"Key_name">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 192, flags = 256}, 399 | #column{name = <<"Seq_in_index">>, org_name = <<"Seq_in_index">>, type = ?TYPE_LONGLONG, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 1, flags = 256}, 400 | #column{name = <<"Column_name">>, org_name = <<"Column_name">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 192, flags = 256}, 401 | #column{name = <<"Collation">>, org_name = <<"Collation">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 3, flags = 256}, 402 | #column{name = <<"Cardinality">>, org_name = <<"Cardinality">>, type = ?TYPE_LONGLONG, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 1, flags = 256}, 403 | #column{name = <<"Sub_part">>, org_name = <<"Sub_part">>, type = ?TYPE_LONGLONG, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 1, flags = 256}, 404 | #column{name = <<"Packed">>, org_name = <<"Packed">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 192, flags = 256}, 405 | #column{name = <<"Null">>, org_name = <<"Null">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 9, flags = 256}, 406 | #column{name = <<"Index_type">>, org_name = <<"Index_type">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 48, flags = 256}, 407 | #column{name = <<"Comment">>, org_name = <<"Comment">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 48, flags = 256}, 408 | #column{name = <<"Index_comment">>, org_name = <<"Index_comment">>, type = ?TYPE_VAR_STRING, table = <<"STATISTICS">>, org_table = <<"STATISTICS">>, schema = <<"information_schema">>, length = 3072, flags = 256} 409 | ], 410 | % Sorry, no indexes 411 | Rows = [], 412 | {reply, #response{status=?STATUS_OK, info = {Header, Rows}}, Srv}; 413 | 414 | 415 | 416 | 417 | default_reply(#request{info = #show{type = create_table, from = Table}}, #server{handler=Handler,state=S}=Srv) -> 418 | Fields = Handler:columns(Table, S), 419 | CreateTable = iolist_to_binary([ 420 | "CREATE TABLE `", Table, "` (\n", 421 | tl(lists:flatmap(fun({Name,Type}) -> 422 | [",", "`", atom_to_binary(Name,latin1), "` ", case Type of string -> "varchar(255)"; integer -> "bigint(20)"; boolean -> "tinyint(1)" end, "\n"] 423 | end, Fields)), 424 | ")" 425 | ]), 426 | Response = { 427 | [#column{name = <<"Table">>, type = ?TYPE_VAR_STRING, flags = 256, decimals = 31, length = 192}, 428 | #column{name = <<"Create Table">>, type = ?TYPE_VAR_STRING, flags = 256, decimals = 31, length = 3072}], 429 | [ 430 | [Table, CreateTable] 431 | ] 432 | }, 433 | {reply, #response{status=?STATUS_OK, info = Response}, Srv}; 434 | 435 | 436 | default_reply(#request{command = field_list, info = Table}, #server{handler=Handler,state=S}=Srv) -> 437 | Fields = Handler:columns(Table, S), 438 | DB = Handler:database(S), 439 | Reply = [#column{schema = DB, table = Table, org_table = Table, name = to_b(Field), org_name = to_b(Field), length = 20, 440 | type = case Type of string -> ?TYPE_VAR_STRING; integer -> ?TYPE_LONGLONG; boolean -> ?TYPE_TINY end} || {Field,Type} <- Fields], 441 | {reply, #response{status=?STATUS_OK, info = {Reply}}, Srv}; 442 | 443 | default_reply(#request{command = ping}, State) -> 444 | {reply, #response{status = ?STATUS_OK, id = 1}, State}; 445 | 446 | 447 | % This line can be disabled to allow passing variable sets to underlying module 448 | default_reply(#request{command = 'query', info = #system_set{}}, State) -> 449 | {reply, #response{status = ?STATUS_OK}, State}; 450 | 451 | 452 | default_reply(#request{info = #select{params=[#value{name = Name, value = undefined}]}}, State) -> 453 | Info = { 454 | [#column{name = Name, type=?TYPE_NULL}], 455 | [] 456 | }, 457 | {reply, #response{status=?STATUS_OK, info=Info}, State}; 458 | 459 | 460 | default_reply(#request{info = #select{params=[#value{name = Name, value = Value}]}}, State) -> 461 | Info = { 462 | [#column{name = Name, type=?TYPE_VARCHAR, length=200}], 463 | [[Value]] 464 | }, 465 | {reply, #response{status=?STATUS_OK, info=Info}, State}; 466 | 467 | 468 | default_reply(#request{info = #select{params = [#function{name = <<"VERSION">>}]}}, State) -> 469 | Version = version_from_handler(State), 470 | ResponseFields = { 471 | [#column{name = <<"VERSION()">>, type=?TYPE_VAR_STRING, length=20}], 472 | [[Version]] 473 | }, 474 | Response = #response{status=?STATUS_OK, info = ResponseFields}, 475 | {reply, Response, State}; 476 | 477 | 478 | default_reply(#request{info = 'begin'}, State) -> 479 | {reply, #response{status = ?STATUS_OK}, State}; 480 | 481 | default_reply(#request{info = commit}, State) -> 482 | {reply, #response{status = ?STATUS_OK}, State}; 483 | 484 | default_reply(#request{info = rollback}, State) -> 485 | {reply, #response{status = ?STATUS_OK}, State}; 486 | 487 | default_reply(#request{info = #select{tables = [#table{name = {<<"information_schema">>,_}}]}}, State) -> 488 | {reply, #response{status = ?STATUS_OK}, State}; 489 | 490 | default_reply(_, _State) -> 491 | undefined. 492 | 493 | 494 | 495 | version_from_handler(#server{handler=H,state=S}) -> 496 | try H:version(S) 497 | catch 498 | _:_ -> <<"5.6.0">> 499 | end. 500 | 501 | 502 | 503 | to_b(Atom) when is_atom(Atom) -> atom_to_binary(Atom, latin1); 504 | to_b(Bin) when is_binary(Bin) -> Bin. 505 | 506 | 507 | or_(undefined,A) -> A; 508 | or_(A,_) -> A. 509 | 510 | 511 | -------------------------------------------------------------------------------- /src/myproto.hrl: -------------------------------------------------------------------------------- 1 | % derived from https://github.com/altenwald/myproto under EPL: 2 | % https://github.com/altenwald/myproto/commit/d89e6cad46fa966e6149aee3ad6f0d711e6182f5 3 | -author('Manuel Rubio '). 4 | 5 | -define(SERVER_SIGN, <<"5.5.6-myproto">>). 6 | 7 | %% status flags 8 | -define(SERVER_STATUS_IN_TRANS, 16#0001). 9 | -define(SERVER_STATUS_AUTOCOMMIT, 16#0002). 10 | -define(SERVER_MORE_RESULTS_EXISTS, 16#0008). 11 | -define(SERVER_STATUS_NO_GOOD_INDEX_USED, 16#0010). 12 | -define(SERVER_STATUS_NO_INDEX_USED, 16#0020). 13 | -define(SERVER_STATUS_CURSOR_EXISTS, 16#0040). 14 | -define(SERVER_STATUS_LAST_ROW_SENT, 16#0080). 15 | -define(SERVER_STATUS_DB_DROPPED, 16#0100). 16 | -define(SERVER_STATUS_NO_BACKSLASH_ESCAPES, 16#0200). 17 | -define(SERVER_STATUS_METADATA_CHANGED, 16#0400). 18 | -define(SERVER_QUERY_WAS_SLOW, 16#0800). 19 | -define(SERVER_PS_OUT_PARAMS, 16#1000). 20 | 21 | % commands 22 | -define(COM_SLEEP, 0). 23 | -define(COM_QUIT, 1). 24 | -define(COM_INIT_DB, 2). 25 | -define(COM_QUERY, 3). 26 | -define(COM_FIELD_LIST, 4). 27 | -define(COM_CREATE_DB, 5). 28 | -define(COM_DROP_DB, 6). 29 | -define(COM_REFRESH, 7). 30 | -define(COM_SHUTDOWN, 8). 31 | -define(COM_STATISTICS, 9). 32 | -define(COM_PROCESS_INFO, 10). 33 | -define(COM_CONNECT, 11). 34 | -define(COM_PROCESS_KILL, 12). 35 | -define(COM_DEBUG, 13). 36 | -define(COM_PING, 14). 37 | -define(COM_TIME, 15). 38 | -define(COM_DELAYED_INSERT, 16). 39 | -define(COM_CHANGE_USER, 17). 40 | -define(COM_BINLOG_DUMP, 18). 41 | -define(COM_TABLE_DUMP, 19). 42 | -define(COM_CONNECT_OUT, 20). 43 | -define(COM_REGISTER_SLAVE, 21). 44 | -define(COM_STMT_PREPARE, 22). 45 | -define(COM_STMT_EXECUTE, 23). 46 | -define(COM_STMT_SEND_LONG_DATA, 24). 47 | -define(COM_STMT_CLOSE, 25). 48 | -define(COM_STMT_RESET, 26). 49 | -define(COM_SET_OPTION, 27). 50 | -define(COM_STMT_FETCH, 28). 51 | -define(COM_DAEMON, 29). 52 | -define(COM_BINLOG_DUMP_GTID, 30). 53 | -define(COM_AUTH, auth). 54 | 55 | % response status 56 | -define(STATUS_OK, 0). 57 | -define(STATUS_HELLO, 16#0A). 58 | -define(STATUS_EOF, 16#FE). 59 | -define(STATUS_ERR, 16#FF). 60 | 61 | % data types 62 | -define(TYPE_DECIMAL, 0). 63 | -define(TYPE_TINY, 1). 64 | -define(TYPE_SHORT, 2). 65 | -define(TYPE_LONG ,3). 66 | -define(TYPE_FLOAT ,4). 67 | -define(TYPE_DOUBLE ,5). 68 | -define(TYPE_NULL ,6). 69 | -define(TYPE_TIMESTAMP ,7). 70 | -define(TYPE_LONGLONG, 8). 71 | -define(TYPE_INT24, 9). 72 | -define(TYPE_DATE, 10). 73 | -define(TYPE_TIME, 11). 74 | -define(TYPE_DATETIME, 12). 75 | -define(TYPE_YEAR, 13). 76 | -define(TYPE_NEWDATE, 14). 77 | -define(TYPE_VARCHAR, 15). 78 | -define(TYPE_BIT, 16). 79 | -define(TYPE_NEWDECIMAL, 16#f6). 80 | -define(TYPE_ENUM, 16#f7). 81 | -define(TYPE_SET, 16#f8). 82 | -define(TYPE_TINY_BLOB, 16#f9). 83 | -define(TYPE_MEDIUM_BLOB, 16#fa). 84 | -define(TYPE_LONG_BLOB, 16#fb). 85 | -define(TYPE_BLOB, 16#fc). 86 | -define(TYPE_VAR_STRING, 16#fd). 87 | -define(TYPE_STRING, 16#fe). 88 | -define(TYPE_GEOMETRY, 16#ff). 89 | 90 | % charsets 91 | -define(BIG5_CHINESE_CI, 1). 92 | -define(LATIN2_CZECH_CS, 2). 93 | -define(DEC8_SWEDISH_CI, 3). 94 | -define(CP850_GENERAL_CI, 4). 95 | -define(LATIN1_GERMAN1_CI, 5). 96 | -define(HP8_ENGLISH_CI, 6). 97 | -define(KOI8R_GENERAL_CI, 7). 98 | -define(LATIN1_SWEDISH_CI, 8). 99 | -define(LATIN2_GENERAL_CI, 9). 100 | -define(SWE7_SWEDISH_CI, 10). 101 | -define(UTF8_GENERAL_CI, 33). 102 | -define(BINARY, 63). 103 | 104 | % capabilities 105 | -define(CLIENT_LONG_PASSWORD, 1). 106 | -define(CLIENT_FOUND_ROWS, 2). 107 | -define(CLIENT_LONG_FLAG, 4). 108 | -define(CLIENT_CONNECT_WITH_DB, 8). 109 | -define(CLIENT_NO_SCHEMA, 16#10). 110 | -define(CLIENT_COMPRESS, 16#20). 111 | -define(CLIENT_ODBC, 16#40). 112 | -define(CLIENT_LOCAL_FILES, 16#80). 113 | -define(CLIENT_IGNORE_SPACE, 16#100). 114 | -define(CLIENT_PROTOCOL_41, 16#200). 115 | -define(CLIENT_INTERACTIVE, 16#400). 116 | -define(CLIENT_SSL, 16#800). 117 | -define(CLIENT_IGNORE_SIGPIPE, 16#1000). 118 | -define(CLIENT_TRANSACTIONS, 16#2000). 119 | -define(CLIENT_RESERVED, 16#4000). 120 | -define(CLIENT_SECURE_CONNECTION, 16#8000). 121 | -define(CLIENT_MULTI_STATEMENTS, 16#10000). 122 | -define(CLIENT_MULTI_RESULTS, 16#20000). 123 | -define(CLIENT_PS_MULTI_RESULTS, 16#40000). 124 | -define(CLIENT_PLUGIN_AUTH, 16#80000). 125 | -define(CLIENT_CONNECT_ATTRS, 16#100000). 126 | -define(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA, 16#200000). 127 | 128 | -define(DATA_NULL, (<<16#fb>>)). 129 | 130 | -include("../include/sql.hrl"). 131 | 132 | -record(user, { 133 | name :: binary(), 134 | password :: binary(), 135 | capabilities :: integer(), 136 | plugin :: binary(), 137 | charset :: binary(), 138 | database :: binary(), 139 | server_hash :: binary() 140 | }). 141 | 142 | -type user() :: #user{}. 143 | 144 | -record(request, { 145 | command :: integer(), 146 | info :: binary() | sql() | user(), 147 | text :: binary(), 148 | continue = false :: boolean(), 149 | id = 0 :: integer() 150 | }). 151 | 152 | -type request() :: #request{}. 153 | 154 | -record(response, { 155 | status = 0 :: integer(), 156 | id = 0 :: integer(), 157 | affected_rows = 0 :: integer(), %% as var_integer 158 | last_insert_id = 0 :: integer(), %% as var_integer 159 | status_flags = 0 :: integer(), 160 | warnings = 0 :: integer(), %% only with protocol 4.1 161 | info = <<>> :: binary(), 162 | error_code = 0 :: integer(), 163 | error_info = <<>> :: binary() 164 | }). 165 | 166 | -type response() :: #response{}. 167 | 168 | -record(column, { 169 | schema = <<>> :: binary(), 170 | table = <<>> :: binary(), 171 | org_name = <<>> :: binary(), 172 | org_table = <<>> :: binary(), 173 | name :: binary(), 174 | charset = ?UTF8_GENERAL_CI :: integer(), 175 | length :: integer(), 176 | type :: integer(), 177 | flags = 0 :: integer(), 178 | decimals = 0 :: integer(), 179 | default = <<>> :: ( binary() | integer() ) 180 | }). 181 | 182 | -type column() :: #column{}. 183 | 184 | -type user_string() :: binary(). 185 | -type password() :: binary(). 186 | -type hash() :: binary(). 187 | 188 | -type state() :: term(). 189 | -------------------------------------------------------------------------------- /src/nanomysql.erl: -------------------------------------------------------------------------------- 1 | -module(nanomysql). 2 | -author('Max Lapshin '). 3 | 4 | -export([connect/1, execute/2, command/3]). 5 | -export([select/2, version/1]). 6 | 7 | -define(TIMEOUT, 8000). 8 | -define(LOCK_TIMEOUT, 5000). 9 | -define(MAXPACKETBYTES, 50000000). 10 | -define(LONG_PASSWORD, 1). 11 | -define(LONG_FLAG, 4). 12 | -define(CLIENT_LOCAL_FILE, 128). 13 | -define(PROTOCOL_41, 512). 14 | -define(CLIENT_MULTI_STATEMENTS, 65536). 15 | -define(CLIENT_MULTI_RESULTS, 131072). 16 | -define(TRANSACTIONS, 8192). 17 | -define(SECURE_CONNECTION, 32768). 18 | -define(CONNECT_WITH_DB, 8). 19 | -define(CONN_TEST_PERIOD, 28000). 20 | -define(TCP_RECV_BUFFER, 8192). 21 | 22 | 23 | -record(conn, { 24 | version, 25 | socket 26 | }). 27 | 28 | %% @doc 29 | %% connect("mysql://user:password@127.0.0.1/dbname") 30 | %% 31 | connect(URL) -> 32 | {ok, {mysql, AuthInfo, Host, Port, "/"++DBName, Qs}} = http_uri:parse(URL, [{scheme_defaults,[{mysql,3306}]}]), 33 | Query = case Qs of 34 | "?" ++ Qs1 -> [ list_to_tuple(string:tokens(Part,"=")) || Part <- string:tokens(Qs1,"&") ]; 35 | "" -> [] 36 | end, 37 | 38 | [User, Password] = case string:tokens(AuthInfo, ":") of 39 | [U,P] -> [U,P]; 40 | [U] -> [U,""] 41 | end, 42 | 43 | {ok, Sock} = gen_tcp:connect(Host, Port, [binary,{active,false}]), 44 | 45 | {ok, 0, <<_ProtoVersion, Rest1/binary>>} = read_packet(Sock), 46 | [ServerVersion, <<_ThreadId:32/little, Scramble1:8/binary, 0, _Caps1:16, Charset, _Status:16, _Caps2:16, 47 | AuthLen, _Reserve1:10/binary, Scramble2:13/binary, Pluginz/binary>>] = binary:split(Rest1, <<0>>), 48 | 21 = AuthLen, 49 | [_Plugin|_] = binary:split(Pluginz, <<0>>), 50 | 51 | <> = <>, 52 | 53 | 54 | MaxPacket = 16777216, 55 | Auth = case Password of 56 | <<>> -> 0; 57 | "" -> 0; 58 | _ -> 59 | Digest1 = crypto:hash(sha, Password), 60 | SHA = crypto:hash_final(crypto:hash_update( 61 | crypto:hash_update(crypto:hash_init(sha), Scramble), 62 | crypto:hash(sha, Digest1) 63 | )), 64 | [size(SHA), crypto:exor(Digest1, SHA) ] 65 | end, 66 | 67 | % http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse41 68 | % CLIENT_CONNECT_WITH_DB = 8 69 | % CapFlags = 16#4003F7CF bor 8, 70 | CapFlags = capabilities([ 71 | ?LONG_PASSWORD , ?CLIENT_LOCAL_FILE, ?LONG_FLAG, ?TRANSACTIONS, 72 | ?CLIENT_MULTI_STATEMENTS, ?CLIENT_MULTI_RESULTS, ?PROTOCOL_41, ?SECURE_CONNECTION 73 | ]), 74 | send_packet(Sock, 1, [<>, binary:copy(<<0>>, 23), 75 | [User, 0], Auth]), % Need to send DBName,0 after auth, but mysql doesn't do it 76 | 77 | case read_packet(Sock) of 78 | {ok, 2, <<_/binary>> = _AuthReply} -> 79 | case proplists:get_value("login", Query) of 80 | "init_db" -> command(init_db, DBName, Sock); 81 | _ -> ok 82 | end, 83 | {ok, #conn{socket = Sock, version = ServerVersion}}; 84 | {error, _} = Error -> 85 | Error 86 | end. 87 | 88 | version(#conn{version = Version}) -> 89 | Version. 90 | 91 | capabilities(Cs) -> 92 | lists:foldl(fun erlang:'bor'/2, 0, Cs). 93 | 94 | 95 | -record(column, { 96 | name, 97 | type, 98 | length 99 | }). 100 | 101 | execute(Query, Sock) -> 102 | command(3, iolist_to_binary(Query), Sock). 103 | 104 | select(Query, Sock) -> 105 | case execute(Query, Sock) of 106 | {error, Error} -> 107 | error(Error); 108 | {ok, Info} when is_map(Info) -> Info; 109 | {ok, {Columns, Rows}} -> 110 | Names = [binary_to_atom(N,latin1) || {N,_} <- Columns], 111 | [maps:from_list(lists:zip(Names,Row)) || Row <- Rows] 112 | end. 113 | 114 | 115 | command(Cmd, Data, Spec) when is_list(Spec) orelse is_binary(Spec) -> 116 | case connect(Spec) of 117 | {ok, Sock} -> 118 | Result = command(Cmd, Data, Sock), 119 | close(Sock), 120 | Result; 121 | {error, E} -> 122 | {error, E} 123 | end; 124 | 125 | command(ping, Info, Sock) -> command(14, Info, Sock); 126 | command(show_fields, Info, Sock) -> command(4, Info, Sock); 127 | command(init_db, Info, Sock) -> command(2, iolist_to_binary(Info), Sock); 128 | 129 | command(2 = Cmd, Info, Sock) -> 130 | send_packet(Sock, 0, [Cmd, Info]), 131 | {ok, _, <<0, _/binary>>} = read_packet(Sock), 132 | ok; 133 | 134 | command(Cmd, Info, Sock) when is_integer(Cmd) -> 135 | send_packet(Sock, 0, [Cmd, Info]), 136 | case read_columns(Sock) of 137 | {error, Error} -> 138 | {error, Error}; 139 | ok -> 140 | {ok, {[],[]}}; 141 | {ok, #{}} = InsertInfo -> 142 | InsertInfo; 143 | {_Cols, Columns} -> % response to query 144 | Rows = read_rows(Columns, Sock), 145 | {ok, {[{Field,type_name(Type)} || #column{name = Field, type = Type} <- Columns], Rows}}; 146 | Columns -> 147 | {ok, {[{Field,type_name(Type)} || #column{name = Field, type = Type} <- Columns]}} 148 | end. 149 | 150 | 151 | read_columns(Sock) -> 152 | case read_packet(Sock) of 153 | {ok, _, <<254, _/binary>>} -> 154 | []; 155 | {ok, _, <<0, Packet/binary>>} -> % 0 is STATUS_OK 156 | {AffectedRows, P1} = varint(Packet), 157 | {LastInsertId, P2} = varint(P1), 158 | <> = P2, 159 | {ok, #{affected_rows => AffectedRows, last_insert_id => LastInsertId, status => Status, warnings => Warnings, info => Rest}}; 160 | {ok, _, <>} -> 161 | {Cols, read_columns(Sock)}; % number of columns 162 | {ok, _, FieldBin} -> 163 | {_Cat, B1} = lenenc_str(FieldBin), % Catalog 164 | {_Schema, B2} = lenenc_str(B1), % schema 165 | {_Table, B3} = lenenc_str(B2), % table 166 | {_OrgTable, B4} = lenenc_str(B3), % org_table 167 | {Field, B5} = lenenc_str(B4), % column name 168 | {_OrgName, B6} = lenenc_str(B5), % org_name 169 | <<16#0c, _Charset:16/little, Length:32/little, Type:8, Flags:16, _Decimals:8, _/binary>> = B6, 170 | case get(debug) of 171 | true -> 172 | io:format("name= ~p, cat= ~p, schema= ~p, table= ~p, org_table= ~p, org_name= ~p, flags=~p, type=~p,decimals=~p,length=~p\n", [ 173 | Field, _Cat, _Schema, _Table, _OrgTable, _OrgName, Flags,Type,_Decimals,Length 174 | ]); 175 | _ -> 176 | ok 177 | end, 178 | [#column{name = Field, type = Type, length = Length}|read_columns(Sock)]; 179 | {error, Error} -> 180 | {error, Error} 181 | end. 182 | 183 | 184 | 185 | 186 | 187 | type_name(0) -> decimal; 188 | type_name(1) -> tiny; 189 | type_name(2) -> short; 190 | type_name(3) -> long; 191 | type_name(7) -> timestamp; 192 | type_name(8) -> longlong; 193 | type_name(15) -> varchar; 194 | type_name(16#fc) -> blob; 195 | type_name(16#fd) -> varstring; 196 | type_name(16#fe) -> string; 197 | type_name(T) -> T. 198 | 199 | 200 | 201 | 202 | read_rows(Columns, Sock) when is_list(Columns) -> 203 | case read_packet(Sock) of 204 | {ok, _, <<254,_/binary>>} -> []; 205 | {ok, _, Row} -> [unpack_row(Columns, Row)|read_rows(Columns, Sock)] 206 | end. 207 | 208 | unpack_row([], <<>>) -> []; 209 | unpack_row([_|Columns], <<16#FB, Rest/binary>>) -> [undefined|unpack_row(Columns, Rest)]; 210 | unpack_row([Column|Columns], Bin) -> 211 | {Value, Rest} = lenenc_str(Bin), 212 | Val = unpack_value(Column, Value), 213 | [Val|unpack_row(Columns, Rest)]. 214 | 215 | 216 | unpack_value(#column{type = 1}, <<"1">>) -> true; 217 | unpack_value(#column{type = 1}, <<"0">>) -> false; 218 | 219 | unpack_value(#column{type = T}, Bin) when 220 | T == 1; T == 2; T == 3; T == 8; T == 9; T == 13 -> 221 | % Len = Length*8, 222 | % <> = Bin, 223 | list_to_integer(binary_to_list(Bin)); 224 | 225 | unpack_value(_, Bin) -> 226 | Bin. 227 | 228 | 229 | 230 | lenenc_str(<>) when Len < 251 -> {Value, Bin}; 231 | lenenc_str(<<252, Len:16/little, Value:Len/binary, Bin/binary>>) -> {Value, Bin}; 232 | lenenc_str(<<253, Len:24/little, Value:Len/binary, Bin/binary>>) -> {Value, Bin}. 233 | 234 | 235 | varint(<<16#fe, Data:64/little, Rest/binary>>) -> {Data, Rest}; 236 | varint(<<16#fd, Data:32/little, Rest/binary>>) -> {Data, Rest}; 237 | varint(<<16#fc, Data:16/little, Rest/binary>>) -> {Data, Rest}; 238 | varint(<>) -> {Data, Rest}. 239 | 240 | 241 | read_packet(#conn{socket = Sock}) -> 242 | read_packet(Sock); 243 | 244 | read_packet(Sock) -> 245 | {ok, <>} = gen_tcp:recv(Sock, 4), 246 | case gen_tcp:recv(Sock, Len) of 247 | {ok, <<255, Code:16/little, Error/binary>>} -> {error, {Code, Error}}; 248 | {ok, Bin} -> {ok, Sequence, Bin} 249 | end. 250 | 251 | close(#conn{socket = Sock}) -> 252 | close(Sock); 253 | close(Sock) -> 254 | gen_tcp:close(Sock). 255 | 256 | 257 | send_packet(#conn{socket = Sock}, Number, Bin) -> 258 | send_packet(Sock, Number, Bin); 259 | 260 | send_packet(Sock, Number, Bin) -> 261 | case iolist_size(Bin) of 262 | Size when Size < 16#ffffff -> 263 | % io:format("out packet: ~p\n", [iolist_to_binary(Bin)]), 264 | ok = gen_tcp:send(Sock, [<>, Bin]); 265 | _ -> 266 | <> = iolist_to_binary(Bin), 267 | send_multi_packet(Sock, Number, Command, Rest) 268 | end. 269 | 270 | send_multi_packet(Sock, Number, Command, <>) -> 271 | ok = gen_tcp:send(Sock, [<<16#ffffff:24/unsigned-little, Number, Command>>, Bin]), 272 | send_multi_packet(Sock, Number, Command, Rest); 273 | 274 | send_multi_packet(Sock, Number, Command, Bin) -> 275 | Size = size(Bin) + 1, 276 | ok = gen_tcp:send(Sock, [<>, Bin]). 277 | -------------------------------------------------------------------------------- /src/sql92.erl: -------------------------------------------------------------------------------- 1 | -module(sql92). 2 | -export([parse/1]). 3 | parse(<>) -> parse(binary_to_list(Bin)); 4 | parse(Q) -> 5 | P = case sql92_scan:string(Q) of 6 | {ok, T, _} -> 7 | case sql92_parser:parse(T) of 8 | {ok, [P_]} -> P_; 9 | PE_ -> PE_ 10 | end; 11 | LE_ -> LE_ 12 | end, 13 | P. -------------------------------------------------------------------------------- /src/sql92_parser.yrl: -------------------------------------------------------------------------------- 1 | %% @author Oleg Smirnov 2 | %% Derived under MIT license from https://github.com/master/mongosql 3 | %% @doc A syntax of a subset of SQL-92 4 | 5 | Nonterminals 6 | assign_commalist assign atom atom_commalist between_pred column 7 | column_commalist column_ref column_ref_commalist comparsion_pred 8 | delete_stmt from_clause in_pred opt_column_commalist 9 | scalar_exp_list insert_stmt like_pred literal opt_order_by_clause 10 | manipulative_stmt opt_group_by_clause sort_spec_commalist 11 | sort_spec sort_key ordering_spec opt_having_clause opt_where_clause 12 | predicate scalar_exp scalar_exp_commalist 13 | search_cond select_stmt sql sql_list tname table_name table_exp 14 | table_ref table_ref_commalist test_for_null_pred update_stmt 15 | values_or_select_stmt where_clause opt_limit_offset_clause 16 | transaction_stmt selection_list table_column variable 17 | insert_row_list insert_row insert_set_pair_list describe_stmt 18 | show_stmt show_type set_stmt set_assign set_assign_list set_value set_target 19 | opt_full opt_from opt_condition selection_item cast_expr selection_item_alias 20 | table_ref_alias opt_collate opt_charset use_stmt. 21 | 22 | Terminals '-' '+' '*' '/' '(' ')' ',' ';' '=' '.' '<' '>' '<=' '>=' '=<' '=>' '!=' '<>' 23 | describe show delete insert select update from where into values null not in 24 | like between or and group by having is set offset limit 25 | begin rollback commit order asc desc string name integer as var full use 26 | tables databases variables fields index create table collation cast collate character. 27 | 28 | Rootsymbol sql_list. 29 | 30 | %% Top level rules 31 | 32 | sql_list -> sql ';' sql_list : ['$1' | '$3']. 33 | sql_list -> sql ';' : ['$1']. 34 | sql_list -> sql : ['$1']. 35 | 36 | sql -> manipulative_stmt : '$1'. 37 | 38 | manipulative_stmt -> transaction_stmt : '$1'. 39 | manipulative_stmt -> delete_stmt : '$1'. 40 | manipulative_stmt -> insert_stmt : '$1'. 41 | manipulative_stmt -> select_stmt : '$1'. 42 | manipulative_stmt -> update_stmt : '$1'. 43 | manipulative_stmt -> describe_stmt : '$1'. 44 | manipulative_stmt -> show_stmt : '$1'. 45 | manipulative_stmt -> set_stmt : '$1'. 46 | manipulative_stmt -> use_stmt : '$1'. 47 | 48 | %% Transactions 49 | 50 | transaction_stmt -> begin : 'begin'. 51 | transaction_stmt -> rollback : 'rollback'. 52 | transaction_stmt -> commit : 'commit'. 53 | 54 | %% DELETE 55 | 56 | delete_stmt -> delete from table_name opt_where_clause : #delete{table='$3', conditions='$4'}. 57 | 58 | %% INSERT statement 59 | 60 | insert_stmt -> insert into table_name set insert_set_pair_list : #insert{table='$3', values=['$5']}. 61 | insert_stmt -> insert into table_name opt_column_commalist values_or_select_stmt : #insert{table='$3', values=zip_insert_values('$4', '$5')}. 62 | 63 | opt_column_commalist -> '$empty' : undefined. 64 | opt_column_commalist -> '(' column_commalist ')' : '$2'. 65 | opt_column_commalist -> column_commalist : '$1'. 66 | 67 | values_or_select_stmt -> values insert_row_list : '$2'. 68 | values_or_select_stmt -> select_stmt : '$1'. 69 | 70 | insert_row_list -> insert_row ',' insert_row_list : ['$1' | '$3']. 71 | insert_row_list -> insert_row : ['$1']. 72 | 73 | insert_row -> '(' scalar_exp_list ')' : '$2'. 74 | 75 | insert_set_pair_list -> assign ',' insert_set_pair_list : ['$1' | '$3']. 76 | insert_set_pair_list -> assign : ['$1']. 77 | 78 | scalar_exp_list -> scalar_exp ',' scalar_exp_list : ['$1' | '$3']. 79 | scalar_exp_list -> scalar_exp : ['$1']. 80 | 81 | atom -> literal : '$1'. 82 | 83 | %% SELECT statement 84 | % select_query <- select_limit / select_order / select_group / select_where / select_from / select_simple ~; 85 | 86 | select_stmt -> select selection_list from_clause table_exp : '$4'#select{params='$2', tables='$3'}. 87 | select_stmt -> select selection_list opt_limit_offset_clause: #select{params='$2', limit=map_key(limit, '$3')}. 88 | 89 | selection_list -> selection_item_alias ',' selection_list : ['$1'|'$3']. 90 | selection_list -> selection_item_alias : ['$1']. 91 | 92 | selection_item_alias -> selection_item 'as' name : apply_alias('$1', value_of('$3')). 93 | selection_item_alias -> selection_item : '$1'. 94 | 95 | selection_item -> '(' select_stmt ')' : #subquery{subquery='$2'}. 96 | selection_item -> scalar_exp opt_collate : '$1'. 97 | 98 | opt_collate -> collate name : {collate, value_of('$2')}. 99 | opt_collate -> '$empty' : undefined. 100 | 101 | %% DESCRIBE 102 | 103 | describe_stmt -> describe table_name : #describe{table='$2'}. 104 | show_stmt -> show opt_full create table name: #show{type=create_table, full='$2', from=value_of('$5')}. 105 | show_stmt -> show opt_full show_type opt_from opt_condition: #show{type='$3', full='$2', from='$4', conditions='$5'}. 106 | show_type -> tables : tables. 107 | show_type -> fields : fields. 108 | show_type -> index : index. 109 | show_type -> databases : databases. 110 | show_type -> variables : variables. 111 | show_type -> collation : collation. 112 | opt_full -> full : true. 113 | opt_full -> '$empty' : false. 114 | opt_from -> from name : value_of('$2'). 115 | opt_from -> '$empty' : undefined. 116 | opt_condition -> like literal : {like, '$2'}. 117 | opt_condition -> where_clause : '$1'. 118 | opt_condition -> '$empty' : undefined. 119 | 120 | set_stmt -> set set_assign_list : #system_set{query='$2'}. 121 | set_assign_list -> set_assign ',' set_assign_list : ['$1' | '$3']. 122 | set_assign_list -> set_assign : ['$1']. 123 | set_assign -> set_target '=' scalar_exp : {'$1', '$3'}. 124 | set_assign -> set_target set_value : {'$1', '$2'}. 125 | set_target -> variable : '$1'. 126 | set_target -> name : #variable{name=value_of('$1'), scope=session}. 127 | set_value -> name : #value{value=value_of('$1')}. 128 | set_value -> literal : #value{value='$1'}. 129 | 130 | use_stmt -> use name : {use, value_of('$2')}. 131 | 132 | %% UPDATE 133 | 134 | update_stmt -> update table_name set assign_commalist opt_where_clause : {update, '$2', '$4', '$5'}. 135 | 136 | assign_commalist -> assign ',' assign_commalist : ['$1' | '$3']. 137 | assign_commalist -> assign : ['$1']. 138 | 139 | assign -> name '.' column '=' scalar_exp : #set{key='$3', value='$5'}. 140 | assign -> column '=' scalar_exp : #set{key='$1', value='$3'}. 141 | 142 | %% Base tables 143 | tname -> name '.' name : {element(1,'$1'),element(2,'$1'),{tolower(value_of('$1')),tolower(value_of('$3'))}}. 144 | tname -> name '.' tables : {element(1,'$1'),element(2,'$1'),{tolower(value_of('$1')),<<"tables">>}}. 145 | tname -> name : {element(1,'$1'),element(2,'$1'), tolower(value_of('$1'))}. 146 | 147 | table_name -> tname 'as' name : #table{name=value_of('$1'), alias=value_of('$3')}. 148 | table_name -> tname name : #table{name=value_of('$1'), alias=value_of('$2')}. 149 | table_name -> tname : #table{name=value_of('$1'), alias=case value_of('$1') of {_,A} -> A; A -> A end}. 150 | 151 | column_commalist -> column ',' column_commalist : ['$1' | '$3']. 152 | column_commalist -> column : ['$1']. 153 | 154 | column_ref -> name '.' '*' : #all{table=value_of('$1')}. 155 | column_ref -> table_column : '$1'. 156 | 157 | table_column -> name '.' name : #key{name=value_of('$3'), alias=value_of('$3'), table=value_of('$1')}. 158 | table_column -> name : #key{name=value_of('$1'), alias=value_of('$1')}. 159 | 160 | % -record(select, {params, tables, conditions, group, order, limit, offset}). 161 | %% Table expressions 162 | table_exp -> 163 | opt_where_clause 164 | opt_order_by_clause 165 | opt_group_by_clause 166 | opt_having_clause 167 | opt_limit_offset_clause : #select{conditions='$1', order='$2', group='$3', limit=map_key(limit, '$5'), offset=map_key(offset, '$5')}. 168 | 169 | from_clause -> from table_ref_commalist : '$2'. 170 | 171 | table_ref_commalist -> table_ref_alias ',' table_ref_commalist : ['$1' | '$3']. 172 | table_ref_commalist -> table_ref_alias : ['$1']. 173 | 174 | table_ref_alias -> table_ref 'as' name : apply_alias('$1', value_of('$3')). 175 | table_ref_alias -> table_ref : '$1'. 176 | 177 | table_ref -> '(' select_stmt ')': #subquery{subquery='$2'}. 178 | table_ref -> table_name : '$1'. 179 | 180 | opt_where_clause -> '$empty' : undefined. 181 | opt_where_clause -> where_clause : '$1'. 182 | 183 | where_clause -> where search_cond : '$2'. 184 | 185 | 186 | opt_order_by_clause -> '$empty' : undefined. 187 | opt_order_by_clause -> order by sort_spec_commalist : '$3'. 188 | 189 | sort_spec_commalist -> sort_spec ',' sort_spec_commalist : ['$1' | '$3']. 190 | sort_spec_commalist -> sort_spec : ['$1']. 191 | 192 | sort_spec -> sort_key ordering_spec : #order{key='$1', sort='$2'}. 193 | sort_spec -> sort_key : #order{key='$1', sort=asc}. 194 | 195 | sort_key -> table_column : '$1'#key.name. 196 | sort_key -> literal : '$1'. 197 | 198 | ordering_spec -> asc : asc. 199 | ordering_spec -> desc : desc. 200 | 201 | 202 | 203 | opt_group_by_clause -> '$empty' : undefined. 204 | opt_group_by_clause -> group by column_ref_commalist : '$3'. 205 | 206 | column_ref_commalist -> name ',' column_ref_commalist : [value_of('$1') | '$3']. 207 | column_ref_commalist -> literal ',' column_ref_commalist : ['$1' | '$3']. 208 | column_ref_commalist -> name : [value_of('$1')]. 209 | column_ref_commalist -> literal : ['$1']. 210 | 211 | opt_having_clause -> '$empty' : undefined. 212 | opt_having_clause -> having search_cond : {having, '$2'}. 213 | 214 | opt_limit_offset_clause -> '$empty' : #{}. 215 | opt_limit_offset_clause -> limit literal offset literal : #{limit => '$2', offset => '$4'}. 216 | opt_limit_offset_clause -> limit literal ',' literal : #{offset => '$2', limit => '$4'}. 217 | opt_limit_offset_clause -> limit literal : #{limit => '$2'}. 218 | opt_limit_offset_clause -> offset literal : #{offset => '$2'}. 219 | 220 | 221 | %% Search conditions 222 | 223 | search_cond -> search_cond or search_cond : #condition{nexo=nexo_or, op1='$1', op2='$3'}. 224 | search_cond -> search_cond and search_cond : #condition{nexo=nexo_and, op1='$1', op2='$3'}. 225 | search_cond -> not search_cond : {'not', '$2'}. 226 | search_cond -> '(' search_cond ')' : '$2'. 227 | search_cond -> predicate : '$1'. 228 | 229 | predicate -> comparsion_pred : '$1'. 230 | predicate -> between_pred : '$1'. 231 | predicate -> like_pred : '$1'. 232 | predicate -> test_for_null_pred : '$1'. 233 | predicate -> in_pred : '$1'. 234 | 235 | % comparsion_pred -> scalar_exp comp scalar_exp : {value_of('$2'), '$1', '$3'}. 236 | % comparsion_pred -> scalar_exp comp '(' select_stmt ')' : {value_of('$2'), '$1', '$4'}. 237 | comparsion_pred -> scalar_exp '=' scalar_exp : #condition{nexo=eq, op1='$1', op2='$3'}. 238 | comparsion_pred -> scalar_exp '>' scalar_exp : #condition{nexo=gt, op1='$1', op2='$3'}. 239 | comparsion_pred -> scalar_exp '<' scalar_exp : #condition{nexo=lt, op1='$1', op2='$3'}. 240 | comparsion_pred -> scalar_exp '>=' scalar_exp : #condition{nexo=gte, op1='$1', op2='$3'}. 241 | comparsion_pred -> scalar_exp '=>' scalar_exp : #condition{nexo=gte, op1='$1', op2='$3'}. 242 | comparsion_pred -> scalar_exp '<=' scalar_exp : #condition{nexo=lte, op1='$1', op2='$3'}. 243 | comparsion_pred -> scalar_exp '=<' scalar_exp : #condition{nexo=lte, op1='$1', op2='$3'}. 244 | comparsion_pred -> scalar_exp '!=' scalar_exp : #condition{nexo=neq, op1='$1', op2='$3'}. 245 | comparsion_pred -> scalar_exp '<>' scalar_exp : #condition{nexo=neq, op1='$1', op2='$3'}. 246 | 247 | between_pred -> scalar_exp not between scalar_exp and scalar_exp : #condition{nexo=not_between, op1='$1', op2={'$4', '$6'}}. 248 | between_pred -> scalar_exp between scalar_exp and scalar_exp : #condition{nexo=between, op1='$1', op2={'$3', '$5'}}. 249 | 250 | like_pred -> scalar_exp not like atom : #condition{nexo=not_like, op1='$1', op2='$4'}. 251 | like_pred -> scalar_exp like atom : #condition{nexo=like, op1='$1', op2='$3'}. 252 | 253 | test_for_null_pred -> column_ref is not scalar_exp : #condition{nexo=is_not, op1='$1', op2='$4'}. 254 | test_for_null_pred -> column_ref is scalar_exp : #condition{nexo=is, op1='$1', op2='$3'}. 255 | 256 | in_pred -> scalar_exp not in '(' select_stmt ')' : #condition{nexo=not_in, op1='$1', op2=#subquery{subquery='$5'}}. 257 | in_pred -> scalar_exp in '(' select_stmt ')' : #condition{nexo=in, op1='$1', op2=#subquery{subquery='$4'}}. 258 | in_pred -> scalar_exp not in '(' atom_commalist ')' : #condition{nexo=not_in, op1='$1', op2=#subquery{subquery='$5'}}. 259 | in_pred -> scalar_exp in '(' atom_commalist ')' : #condition{nexo=in, op1='$1', op2=#subquery{subquery='$4'}}. 260 | 261 | atom_commalist -> atom ',' atom_commalist : ['$1' | '$3']. 262 | atom_commalist -> atom : ['$1']. 263 | 264 | %% Scalar expressions 265 | scalar_exp -> cast_expr : '$1'. 266 | scalar_exp -> name '(' scalar_exp_commalist ')' : #function{name=value_of('$1'), params='$3'}. 267 | scalar_exp -> scalar_exp '+' scalar_exp : #operation{type= <<"+">>, op1='$1', op2='$3'}. 268 | scalar_exp -> scalar_exp '-' scalar_exp : #operation{type= <<"-">>, op1='$1', op2='$3'}. 269 | scalar_exp -> scalar_exp '*' scalar_exp : #operation{type= <<"*">>, op1='$1', op2='$3'}. 270 | scalar_exp -> scalar_exp '/' scalar_exp : #operation{type= <<"/">>, op1='$1', op2='$3'}. 271 | scalar_exp -> '(' scalar_exp ')' : '$2'. 272 | scalar_exp -> '*' : #all{}. 273 | % scalar_exp -> atom 'as' name : #value{value='$1', name=value_of('$3')}. 274 | scalar_exp -> column_ref : '$1'. 275 | scalar_exp -> atom : #value{value='$1'}. 276 | scalar_exp -> null : #value{value=undefined}. 277 | scalar_exp -> variable : '$1'. 278 | 279 | variable -> var : #variable{name=value_of('$1'), scope=local}. 280 | 281 | cast_expr -> cast '(' scalar_exp 'as' name '(' integer ')' opt_charset ')' : #function{name = <<"cast">>, params=['$3', {value_of('$5'), value_of('$7')}]}. 282 | cast_expr -> cast '(' scalar_exp 'as' name opt_charset ')' : #function{name = <<"cast">>, params=['$3', value_of('$5')]}. 283 | 284 | opt_charset -> character set name : value_of('$3'). 285 | opt_charset -> '$empty' : undefined. 286 | 287 | scalar_exp_commalist -> scalar_exp ',' scalar_exp_commalist : ['$1'|'$3']. 288 | scalar_exp_commalist -> scalar_exp : ['$1']. 289 | scalar_exp_commalist -> '$empty' : []. 290 | 291 | 292 | column -> name : value_of('$1'). 293 | literal -> integer : value_of('$1'). 294 | literal -> string : value_of('$1'). 295 | 296 | 297 | Erlang code. 298 | -include("../include/sql.hrl"). 299 | -export([compare/1]). 300 | 301 | value_of(Token) -> element(3, Token). 302 | map_key(Key, #{}=KV) -> maps:get(Key, KV, undefined); 303 | map_key(_, _) -> undefined. 304 | 305 | tolower(Bin) when is_binary(Bin) -> list_to_binary(string:to_lower(binary_to_list(Bin))). 306 | 307 | apply_alias(V, undefined) -> V; 308 | apply_alias(#function{}=V, A) -> V#function{alias=A}; 309 | apply_alias(#subquery{}=V, A) -> V#subquery{name=A}; 310 | apply_alias(#value{}=V, A) -> V#value{name=A}; 311 | apply_alias(#key{}=V, A) -> V#key{alias=A}; 312 | apply_alias(V, _) -> V. 313 | 314 | zip_insert_values(undefined, Rows) -> Rows; 315 | zip_insert_values(Keys, Rows) -> 316 | [ [#set{key=Key, value=Val} 317 | || {Key, Val} <- lists:zip(Keys, Row)] 318 | || Row <- Rows]. 319 | 320 | 321 | compare(Q) -> 322 | {T1,N} = {0, undefined}, % timer:tc(fun() -> mysql_proto_old:parse(Q) end), 323 | {T3,{T2, P}} = timer:tc(fun() -> 324 | case timer:tc(fun() -> sql92_scan:string(Q) end) of 325 | {T2_, {ok, T, _}} -> 326 | % ct:pal("SCAN ~p", [T]), 327 | case sql92_parser:parse(T) of 328 | {ok, [P_]} -> {T2_, P_}; 329 | PE_ -> {parse_error, PE_} 330 | end; 331 | LE_ -> {lex_error, LE_} 332 | end 333 | end), 334 | 335 | {true, {T1, vs, T2, T3}, N, P}. -------------------------------------------------------------------------------- /src/sql92_scan.erl: -------------------------------------------------------------------------------- 1 | -file("/usr/local/Cellar/erlang/20.0/lib/erlang/lib/parsetools-2.1.5/include/leexinc.hrl", 0). 2 | %% The source of this file is part of leex distribution, as such it 3 | %% has the same Copyright as the other files in the leex 4 | %% distribution. The Copyright is defined in the accompanying file 5 | %% COPYRIGHT. However, the resultant scanner generated by leex is the 6 | %% property of the creator of the scanner and is not covered by that 7 | %% Copyright. 8 | 9 | -module(sql92_scan). 10 | 11 | -export([string/1,string/2,token/2,token/3,tokens/2,tokens/3]). 12 | -export([format_error/1]). 13 | 14 | %% User code. This is placed here to allow extra attributes. 15 | -file("src/sql92_scan.xrl", 29). 16 | 17 | check_reserved(TokenChars, TokenLine) -> 18 | R = atom(string_gen(TokenChars)), 19 | case reserved_word(R) of 20 | true -> {token, {R, TokenLine}}; 21 | false -> {token, {name, TokenLine, bitstring(TokenChars)}} 22 | end. 23 | 24 | atom(TokenChars) -> list_to_atom(string:to_lower(TokenChars)). 25 | integer(TokenChars) -> list_to_integer(TokenChars). 26 | bitstring(TokenChars) -> list_to_bitstring(TokenChars). 27 | strip(TokenChars,TokenLen) -> lists:sublist(TokenChars, 2, TokenLen - 2). 28 | variable([_,_|TokenChars]) -> list_to_bitstring(TokenChars). 29 | 30 | reserved_word('all') -> true; 31 | reserved_word('and') -> true; 32 | reserved_word('asc') -> true; 33 | reserved_word('begin') -> true; 34 | reserved_word('between') -> true; 35 | reserved_word('by') -> true; 36 | reserved_word('commit') -> true; 37 | reserved_word('delete') -> true; 38 | reserved_word('desc') -> true; 39 | reserved_word('distinct') -> true; 40 | reserved_word('from') -> true; 41 | reserved_word('group') -> true; 42 | reserved_word('having') -> true; 43 | reserved_word('insert') -> true; 44 | reserved_word('in') -> true; 45 | reserved_word('into') -> true; 46 | reserved_word('is') -> true; 47 | reserved_word('like') -> true; 48 | reserved_word('limit') -> true; 49 | reserved_word('not') -> true; 50 | reserved_word('null') -> true; 51 | reserved_word('offset') -> true; 52 | reserved_word('or') -> true; 53 | reserved_word('order') -> true; 54 | reserved_word('rollback') -> true; 55 | reserved_word('select') -> true; 56 | reserved_word('describe') -> true; 57 | reserved_word('show') -> true; 58 | reserved_word('full') -> true; 59 | reserved_word('set') -> true; 60 | reserved_word('update') -> true; 61 | reserved_word('values') -> true; 62 | reserved_word('where') -> true; 63 | reserved_word('as') -> true; 64 | reserved_word('fields') -> true; 65 | reserved_word('index') -> true; 66 | reserved_word('create') -> true; 67 | reserved_word('table') -> true; 68 | reserved_word('tables') -> true; 69 | reserved_word('databases') -> true; 70 | reserved_word('variables') -> true; 71 | reserved_word('collation') -> true; 72 | reserved_word('collate') -> true; 73 | reserved_word('character') -> true; 74 | reserved_word('cast') -> true; 75 | reserved_word('use') -> true; 76 | reserved_word(_) -> false. 77 | 78 | string_gen([A,A|Cs]) when A == $'; A == $" -> 79 | [A|string_gen(Cs)]; 80 | string_gen([$\\|Cs]) -> 81 | string_escape(Cs); 82 | string_gen([C|Cs]) -> 83 | [C|string_gen(Cs)]; 84 | string_gen([]) -> []. 85 | 86 | string_escape([C|Cs]) -> 87 | case escape_char(C) of 88 | not_control -> [$\\, C|string_gen(Cs)]; 89 | Control -> [Control|string_gen(Cs)] 90 | end; 91 | string_escape([]) -> [$\\]. 92 | 93 | %% https://dev.mysql.com/doc/refman/5.7/en/string-literals.html#character-escape-sequences 94 | escape_char($0) -> $\0; 95 | escape_char($') -> $\'; 96 | escape_char($") -> $\"; 97 | escape_char($b) -> $\b; 98 | escape_char($n) -> $\n; 99 | escape_char($r) -> $\r; 100 | escape_char($t) -> $\t; 101 | escape_char($Z) -> 10#26; 102 | escape_char($\\) -> $\\; 103 | %% escape_char($%) -> $%; % TODO except like 104 | %% escape_char($_) -> $_; % TODO except like 105 | escape_char(_) -> not_control. 106 | 107 | -file("/usr/local/Cellar/erlang/20.0/lib/erlang/lib/parsetools-2.1.5/include/leexinc.hrl", 14). 108 | 109 | format_error({illegal,S}) -> ["illegal characters ",io_lib:write_string(S)]; 110 | format_error({user,S}) -> S. 111 | 112 | string(String) -> string(String, 1). 113 | 114 | string(String, Line) -> string(String, Line, String, []). 115 | 116 | %% string(InChars, Line, TokenChars, Tokens) -> 117 | %% {ok,Tokens,Line} | {error,ErrorInfo,Line}. 118 | %% Note the line number going into yystate, L0, is line of token 119 | %% start while line number returned is line of token end. We want line 120 | %% of token start. 121 | 122 | string([], L, [], Ts) -> % No partial tokens! 123 | {ok,yyrev(Ts),L}; 124 | string(Ics0, L0, Tcs, Ts) -> 125 | case yystate(yystate(), Ics0, L0, 0, reject, 0) of 126 | {A,Alen,Ics1,L1} -> % Accepting end state 127 | string_cont(Ics1, L1, yyaction(A, Alen, Tcs, L0), Ts); 128 | {A,Alen,Ics1,L1,_S1} -> % Accepting transistion state 129 | string_cont(Ics1, L1, yyaction(A, Alen, Tcs, L0), Ts); 130 | {reject,_Alen,Tlen,_Ics1,L1,_S1} -> % After a non-accepting state 131 | {error,{L0,?MODULE,{illegal,yypre(Tcs, Tlen+1)}},L1}; 132 | {A,Alen,Tlen,_Ics1,L1,_S1} -> 133 | Tcs1 = yysuf(Tcs, Alen), 134 | L2 = adjust_line(Tlen, Alen, Tcs1, L1), 135 | string_cont(Tcs1, L2, yyaction(A, Alen, Tcs, L0), Ts) 136 | end. 137 | 138 | %% string_cont(RestChars, Line, Token, Tokens) 139 | %% Test for and remove the end token wrapper. Push back characters 140 | %% are prepended to RestChars. 141 | 142 | -dialyzer({nowarn_function, string_cont/4}). 143 | 144 | string_cont(Rest, Line, {token,T}, Ts) -> 145 | string(Rest, Line, Rest, [T|Ts]); 146 | string_cont(Rest, Line, {token,T,Push}, Ts) -> 147 | NewRest = Push ++ Rest, 148 | string(NewRest, Line, NewRest, [T|Ts]); 149 | string_cont(Rest, Line, {end_token,T}, Ts) -> 150 | string(Rest, Line, Rest, [T|Ts]); 151 | string_cont(Rest, Line, {end_token,T,Push}, Ts) -> 152 | NewRest = Push ++ Rest, 153 | string(NewRest, Line, NewRest, [T|Ts]); 154 | string_cont(Rest, Line, skip_token, Ts) -> 155 | string(Rest, Line, Rest, Ts); 156 | string_cont(Rest, Line, {skip_token,Push}, Ts) -> 157 | NewRest = Push ++ Rest, 158 | string(NewRest, Line, NewRest, Ts); 159 | string_cont(_Rest, Line, {error,S}, _Ts) -> 160 | {error,{Line,?MODULE,{user,S}},Line}. 161 | 162 | %% token(Continuation, Chars) -> 163 | %% token(Continuation, Chars, Line) -> 164 | %% {more,Continuation} | {done,ReturnVal,RestChars}. 165 | %% Must be careful when re-entering to append the latest characters to the 166 | %% after characters in an accept. The continuation is: 167 | %% {token,State,CurrLine,TokenChars,TokenLen,TokenLine,AccAction,AccLen} 168 | 169 | token(Cont, Chars) -> token(Cont, Chars, 1). 170 | 171 | token([], Chars, Line) -> 172 | token(yystate(), Chars, Line, Chars, 0, Line, reject, 0); 173 | token({token,State,Line,Tcs,Tlen,Tline,Action,Alen}, Chars, _) -> 174 | token(State, Chars, Line, Tcs ++ Chars, Tlen, Tline, Action, Alen). 175 | 176 | %% token(State, InChars, Line, TokenChars, TokenLen, TokenLine, 177 | %% AcceptAction, AcceptLen) -> 178 | %% {more,Continuation} | {done,ReturnVal,RestChars}. 179 | %% The argument order is chosen to be more efficient. 180 | 181 | token(S0, Ics0, L0, Tcs, Tlen0, Tline, A0, Alen0) -> 182 | case yystate(S0, Ics0, L0, Tlen0, A0, Alen0) of 183 | %% Accepting end state, we have a token. 184 | {A1,Alen1,Ics1,L1} -> 185 | token_cont(Ics1, L1, yyaction(A1, Alen1, Tcs, Tline)); 186 | %% Accepting transition state, can take more chars. 187 | {A1,Alen1,[],L1,S1} -> % Need more chars to check 188 | {more,{token,S1,L1,Tcs,Alen1,Tline,A1,Alen1}}; 189 | {A1,Alen1,Ics1,L1,_S1} -> % Take what we got 190 | token_cont(Ics1, L1, yyaction(A1, Alen1, Tcs, Tline)); 191 | %% After a non-accepting state, maybe reach accept state later. 192 | {A1,Alen1,Tlen1,[],L1,S1} -> % Need more chars to check 193 | {more,{token,S1,L1,Tcs,Tlen1,Tline,A1,Alen1}}; 194 | {reject,_Alen1,Tlen1,eof,L1,_S1} -> % No token match 195 | %% Check for partial token which is error. 196 | Ret = if Tlen1 > 0 -> {error,{Tline,?MODULE, 197 | %% Skip eof tail in Tcs. 198 | {illegal,yypre(Tcs, Tlen1)}},L1}; 199 | true -> {eof,L1} 200 | end, 201 | {done,Ret,eof}; 202 | {reject,_Alen1,Tlen1,Ics1,L1,_S1} -> % No token match 203 | Error = {Tline,?MODULE,{illegal,yypre(Tcs, Tlen1+1)}}, 204 | {done,{error,Error,L1},Ics1}; 205 | {A1,Alen1,Tlen1,_Ics1,L1,_S1} -> % Use last accept match 206 | Tcs1 = yysuf(Tcs, Alen1), 207 | L2 = adjust_line(Tlen1, Alen1, Tcs1, L1), 208 | token_cont(Tcs1, L2, yyaction(A1, Alen1, Tcs, Tline)) 209 | end. 210 | 211 | %% token_cont(RestChars, Line, Token) 212 | %% If we have a token or error then return done, else if we have a 213 | %% skip_token then continue. 214 | 215 | -dialyzer({nowarn_function, token_cont/3}). 216 | 217 | token_cont(Rest, Line, {token,T}) -> 218 | {done,{ok,T,Line},Rest}; 219 | token_cont(Rest, Line, {token,T,Push}) -> 220 | NewRest = Push ++ Rest, 221 | {done,{ok,T,Line},NewRest}; 222 | token_cont(Rest, Line, {end_token,T}) -> 223 | {done,{ok,T,Line},Rest}; 224 | token_cont(Rest, Line, {end_token,T,Push}) -> 225 | NewRest = Push ++ Rest, 226 | {done,{ok,T,Line},NewRest}; 227 | token_cont(Rest, Line, skip_token) -> 228 | token(yystate(), Rest, Line, Rest, 0, Line, reject, 0); 229 | token_cont(Rest, Line, {skip_token,Push}) -> 230 | NewRest = Push ++ Rest, 231 | token(yystate(), NewRest, Line, NewRest, 0, Line, reject, 0); 232 | token_cont(Rest, Line, {error,S}) -> 233 | {done,{error,{Line,?MODULE,{user,S}},Line},Rest}. 234 | 235 | %% tokens(Continuation, Chars, Line) -> 236 | %% {more,Continuation} | {done,ReturnVal,RestChars}. 237 | %% Must be careful when re-entering to append the latest characters to the 238 | %% after characters in an accept. The continuation is: 239 | %% {tokens,State,CurrLine,TokenChars,TokenLen,TokenLine,Tokens,AccAction,AccLen} 240 | %% {skip_tokens,State,CurrLine,TokenChars,TokenLen,TokenLine,Error,AccAction,AccLen} 241 | 242 | tokens(Cont, Chars) -> tokens(Cont, Chars, 1). 243 | 244 | tokens([], Chars, Line) -> 245 | tokens(yystate(), Chars, Line, Chars, 0, Line, [], reject, 0); 246 | tokens({tokens,State,Line,Tcs,Tlen,Tline,Ts,Action,Alen}, Chars, _) -> 247 | tokens(State, Chars, Line, Tcs ++ Chars, Tlen, Tline, Ts, Action, Alen); 248 | tokens({skip_tokens,State,Line,Tcs,Tlen,Tline,Error,Action,Alen}, Chars, _) -> 249 | skip_tokens(State, Chars, Line, Tcs ++ Chars, Tlen, Tline, Error, Action, Alen). 250 | 251 | %% tokens(State, InChars, Line, TokenChars, TokenLen, TokenLine, Tokens, 252 | %% AcceptAction, AcceptLen) -> 253 | %% {more,Continuation} | {done,ReturnVal,RestChars}. 254 | 255 | tokens(S0, Ics0, L0, Tcs, Tlen0, Tline, Ts, A0, Alen0) -> 256 | case yystate(S0, Ics0, L0, Tlen0, A0, Alen0) of 257 | %% Accepting end state, we have a token. 258 | {A1,Alen1,Ics1,L1} -> 259 | tokens_cont(Ics1, L1, yyaction(A1, Alen1, Tcs, Tline), Ts); 260 | %% Accepting transition state, can take more chars. 261 | {A1,Alen1,[],L1,S1} -> % Need more chars to check 262 | {more,{tokens,S1,L1,Tcs,Alen1,Tline,Ts,A1,Alen1}}; 263 | {A1,Alen1,Ics1,L1,_S1} -> % Take what we got 264 | tokens_cont(Ics1, L1, yyaction(A1, Alen1, Tcs, Tline), Ts); 265 | %% After a non-accepting state, maybe reach accept state later. 266 | {A1,Alen1,Tlen1,[],L1,S1} -> % Need more chars to check 267 | {more,{tokens,S1,L1,Tcs,Tlen1,Tline,Ts,A1,Alen1}}; 268 | {reject,_Alen1,Tlen1,eof,L1,_S1} -> % No token match 269 | %% Check for partial token which is error, no need to skip here. 270 | Ret = if Tlen1 > 0 -> {error,{Tline,?MODULE, 271 | %% Skip eof tail in Tcs. 272 | {illegal,yypre(Tcs, Tlen1)}},L1}; 273 | Ts == [] -> {eof,L1}; 274 | true -> {ok,yyrev(Ts),L1} 275 | end, 276 | {done,Ret,eof}; 277 | {reject,_Alen1,Tlen1,_Ics1,L1,_S1} -> 278 | %% Skip rest of tokens. 279 | Error = {L1,?MODULE,{illegal,yypre(Tcs, Tlen1+1)}}, 280 | skip_tokens(yysuf(Tcs, Tlen1+1), L1, Error); 281 | {A1,Alen1,Tlen1,_Ics1,L1,_S1} -> 282 | Token = yyaction(A1, Alen1, Tcs, Tline), 283 | Tcs1 = yysuf(Tcs, Alen1), 284 | L2 = adjust_line(Tlen1, Alen1, Tcs1, L1), 285 | tokens_cont(Tcs1, L2, Token, Ts) 286 | end. 287 | 288 | %% tokens_cont(RestChars, Line, Token, Tokens) 289 | %% If we have an end_token or error then return done, else if we have 290 | %% a token then save it and continue, else if we have a skip_token 291 | %% just continue. 292 | 293 | -dialyzer({nowarn_function, tokens_cont/4}). 294 | 295 | tokens_cont(Rest, Line, {token,T}, Ts) -> 296 | tokens(yystate(), Rest, Line, Rest, 0, Line, [T|Ts], reject, 0); 297 | tokens_cont(Rest, Line, {token,T,Push}, Ts) -> 298 | NewRest = Push ++ Rest, 299 | tokens(yystate(), NewRest, Line, NewRest, 0, Line, [T|Ts], reject, 0); 300 | tokens_cont(Rest, Line, {end_token,T}, Ts) -> 301 | {done,{ok,yyrev(Ts, [T]),Line},Rest}; 302 | tokens_cont(Rest, Line, {end_token,T,Push}, Ts) -> 303 | NewRest = Push ++ Rest, 304 | {done,{ok,yyrev(Ts, [T]),Line},NewRest}; 305 | tokens_cont(Rest, Line, skip_token, Ts) -> 306 | tokens(yystate(), Rest, Line, Rest, 0, Line, Ts, reject, 0); 307 | tokens_cont(Rest, Line, {skip_token,Push}, Ts) -> 308 | NewRest = Push ++ Rest, 309 | tokens(yystate(), NewRest, Line, NewRest, 0, Line, Ts, reject, 0); 310 | tokens_cont(Rest, Line, {error,S}, _Ts) -> 311 | skip_tokens(Rest, Line, {Line,?MODULE,{user,S}}). 312 | 313 | %%skip_tokens(InChars, Line, Error) -> {done,{error,Error,Line},Ics}. 314 | %% Skip tokens until an end token, junk everything and return the error. 315 | 316 | skip_tokens(Ics, Line, Error) -> 317 | skip_tokens(yystate(), Ics, Line, Ics, 0, Line, Error, reject, 0). 318 | 319 | %% skip_tokens(State, InChars, Line, TokenChars, TokenLen, TokenLine, Tokens, 320 | %% AcceptAction, AcceptLen) -> 321 | %% {more,Continuation} | {done,ReturnVal,RestChars}. 322 | 323 | skip_tokens(S0, Ics0, L0, Tcs, Tlen0, Tline, Error, A0, Alen0) -> 324 | case yystate(S0, Ics0, L0, Tlen0, A0, Alen0) of 325 | {A1,Alen1,Ics1,L1} -> % Accepting end state 326 | skip_cont(Ics1, L1, yyaction(A1, Alen1, Tcs, Tline), Error); 327 | {A1,Alen1,[],L1,S1} -> % After an accepting state 328 | {more,{skip_tokens,S1,L1,Tcs,Alen1,Tline,Error,A1,Alen1}}; 329 | {A1,Alen1,Ics1,L1,_S1} -> 330 | skip_cont(Ics1, L1, yyaction(A1, Alen1, Tcs, Tline), Error); 331 | {A1,Alen1,Tlen1,[],L1,S1} -> % After a non-accepting state 332 | {more,{skip_tokens,S1,L1,Tcs,Tlen1,Tline,Error,A1,Alen1}}; 333 | {reject,_Alen1,_Tlen1,eof,L1,_S1} -> 334 | {done,{error,Error,L1},eof}; 335 | {reject,_Alen1,Tlen1,_Ics1,L1,_S1} -> 336 | skip_tokens(yysuf(Tcs, Tlen1+1), L1, Error); 337 | {A1,Alen1,Tlen1,_Ics1,L1,_S1} -> 338 | Token = yyaction(A1, Alen1, Tcs, Tline), 339 | Tcs1 = yysuf(Tcs, Alen1), 340 | L2 = adjust_line(Tlen1, Alen1, Tcs1, L1), 341 | skip_cont(Tcs1, L2, Token, Error) 342 | end. 343 | 344 | %% skip_cont(RestChars, Line, Token, Error) 345 | %% Skip tokens until we have an end_token or error then return done 346 | %% with the original rror. 347 | 348 | -dialyzer({nowarn_function, skip_cont/4}). 349 | 350 | skip_cont(Rest, Line, {token,_T}, Error) -> 351 | skip_tokens(yystate(), Rest, Line, Rest, 0, Line, Error, reject, 0); 352 | skip_cont(Rest, Line, {token,_T,Push}, Error) -> 353 | NewRest = Push ++ Rest, 354 | skip_tokens(yystate(), NewRest, Line, NewRest, 0, Line, Error, reject, 0); 355 | skip_cont(Rest, Line, {end_token,_T}, Error) -> 356 | {done,{error,Error,Line},Rest}; 357 | skip_cont(Rest, Line, {end_token,_T,Push}, Error) -> 358 | NewRest = Push ++ Rest, 359 | {done,{error,Error,Line},NewRest}; 360 | skip_cont(Rest, Line, skip_token, Error) -> 361 | skip_tokens(yystate(), Rest, Line, Rest, 0, Line, Error, reject, 0); 362 | skip_cont(Rest, Line, {skip_token,Push}, Error) -> 363 | NewRest = Push ++ Rest, 364 | skip_tokens(yystate(), NewRest, Line, NewRest, 0, Line, Error, reject, 0); 365 | skip_cont(Rest, Line, {error,_S}, Error) -> 366 | skip_tokens(yystate(), Rest, Line, Rest, 0, Line, Error, reject, 0). 367 | 368 | yyrev(List) -> lists:reverse(List). 369 | yyrev(List, Tail) -> lists:reverse(List, Tail). 370 | yypre(List, N) -> lists:sublist(List, N). 371 | yysuf(List, N) -> lists:nthtail(N, List). 372 | 373 | %% adjust_line(TokenLength, AcceptLength, Chars, Line) -> NewLine 374 | %% Make sure that newlines in Chars are not counted twice. 375 | %% Line has been updated with respect to newlines in the prefix of 376 | %% Chars consisting of (TokenLength - AcceptLength) characters. 377 | 378 | adjust_line(N, N, _Cs, L) -> L; 379 | adjust_line(T, A, [$\n|Cs], L) -> 380 | adjust_line(T-1, A, Cs, L-1); 381 | adjust_line(T, A, [_|Cs], L) -> 382 | adjust_line(T-1, A, Cs, L). 383 | 384 | %% yystate() -> InitialState. 385 | %% yystate(State, InChars, Line, CurrTokLen, AcceptAction, AcceptLen) -> 386 | %% {Action, AcceptLen, RestChars, Line} | 387 | %% {Action, AcceptLen, RestChars, Line, State} | 388 | %% {reject, AcceptLen, CurrTokLen, RestChars, Line, State} | 389 | %% {Action, AcceptLen, CurrTokLen, RestChars, Line, State}. 390 | %% Generated state transition functions. The non-accepting end state 391 | %% return signal either an unrecognised character or end of current 392 | %% input. 393 | 394 | -file("src/sql92_scan.erl", 393). 395 | yystate() -> 25. 396 | 397 | yystate(28, Ics, Line, Tlen, _, _) -> 398 | {2,Tlen,Ics,Line}; 399 | yystate(27, [37|Ics], Line, Tlen, _, _) -> 400 | yystate(4, Ics, Line, Tlen+1, 8, Tlen); 401 | yystate(27, [10|Ics], Line, Tlen, _, _) -> 402 | yystate(27, Ics, Line+1, Tlen+1, 8, Tlen); 403 | yystate(27, [C|Ics], Line, Tlen, _, _) when C >= 0, C =< 9 -> 404 | yystate(27, Ics, Line, Tlen+1, 8, Tlen); 405 | yystate(27, [C|Ics], Line, Tlen, _, _) when C >= 11, C =< 32 -> 406 | yystate(27, Ics, Line, Tlen+1, 8, Tlen); 407 | yystate(27, Ics, Line, Tlen, _, _) -> 408 | {8,Tlen,Ics,Line,27}; 409 | yystate(26, [C|Ics], Line, Tlen, _, _) when C >= 48, C =< 57 -> 410 | yystate(26, Ics, Line, Tlen+1, 1, Tlen); 411 | yystate(26, Ics, Line, Tlen, _, _) -> 412 | {1,Tlen,Ics,Line,26}; 413 | yystate(25, [96|Ics], Line, Tlen, Action, Alen) -> 414 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 415 | yystate(25, [95|Ics], Line, Tlen, Action, Alen) -> 416 | yystate(1, Ics, Line, Tlen+1, Action, Alen); 417 | yystate(25, [64|Ics], Line, Tlen, Action, Alen) -> 418 | yystate(2, Ics, Line, Tlen+1, Action, Alen); 419 | yystate(25, [62|Ics], Line, Tlen, Action, Alen) -> 420 | yystate(14, Ics, Line, Tlen+1, Action, Alen); 421 | yystate(25, [61|Ics], Line, Tlen, Action, Alen) -> 422 | yystate(18, Ics, Line, Tlen+1, Action, Alen); 423 | yystate(25, [60|Ics], Line, Tlen, Action, Alen) -> 424 | yystate(22, Ics, Line, Tlen+1, Action, Alen); 425 | yystate(25, [59|Ics], Line, Tlen, Action, Alen) -> 426 | yystate(28, Ics, Line, Tlen+1, Action, Alen); 427 | yystate(25, [39|Ics], Line, Tlen, Action, Alen) -> 428 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 429 | yystate(25, [37|Ics], Line, Tlen, Action, Alen) -> 430 | yystate(4, Ics, Line, Tlen+1, Action, Alen); 431 | yystate(25, [34|Ics], Line, Tlen, Action, Alen) -> 432 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 433 | yystate(25, [33|Ics], Line, Tlen, Action, Alen) -> 434 | yystate(19, Ics, Line, Tlen+1, Action, Alen); 435 | yystate(25, [10|Ics], Line, Tlen, Action, Alen) -> 436 | yystate(27, Ics, Line+1, Tlen+1, Action, Alen); 437 | yystate(25, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 438 | yystate(27, Ics, Line, Tlen+1, Action, Alen); 439 | yystate(25, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 32 -> 440 | yystate(27, Ics, Line, Tlen+1, Action, Alen); 441 | yystate(25, [C|Ics], Line, Tlen, Action, Alen) when C >= 40, C =< 47 -> 442 | yystate(28, Ics, Line, Tlen+1, Action, Alen); 443 | yystate(25, [C|Ics], Line, Tlen, Action, Alen) when C >= 48, C =< 57 -> 444 | yystate(26, Ics, Line, Tlen+1, Action, Alen); 445 | yystate(25, [C|Ics], Line, Tlen, Action, Alen) when C >= 65, C =< 90 -> 446 | yystate(1, Ics, Line, Tlen+1, Action, Alen); 447 | yystate(25, [C|Ics], Line, Tlen, Action, Alen) when C >= 97, C =< 122 -> 448 | yystate(1, Ics, Line, Tlen+1, Action, Alen); 449 | yystate(25, Ics, Line, Tlen, Action, Alen) -> 450 | {Action,Alen,Tlen,Ics,Line,25}; 451 | yystate(24, [92|Ics], Line, Tlen, Action, Alen) -> 452 | yystate(20, Ics, Line, Tlen+1, Action, Alen); 453 | yystate(24, [39|Ics], Line, Tlen, Action, Alen) -> 454 | yystate(16, Ics, Line, Tlen+1, Action, Alen); 455 | yystate(24, [10|Ics], Line, Tlen, Action, Alen) -> 456 | yystate(8, Ics, Line+1, Tlen+1, Action, Alen); 457 | yystate(24, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 458 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 459 | yystate(24, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 38 -> 460 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 461 | yystate(24, [C|Ics], Line, Tlen, Action, Alen) when C >= 40, C =< 91 -> 462 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 463 | yystate(24, [C|Ics], Line, Tlen, Action, Alen) when C >= 93 -> 464 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 465 | yystate(24, Ics, Line, Tlen, Action, Alen) -> 466 | {Action,Alen,Tlen,Ics,Line,24}; 467 | yystate(23, Ics, Line, Tlen, _, _) -> 468 | {0,Tlen,Ics,Line}; 469 | yystate(22, [62|Ics], Line, Tlen, _, _) -> 470 | yystate(23, Ics, Line, Tlen+1, 0, Tlen); 471 | yystate(22, [61|Ics], Line, Tlen, _, _) -> 472 | yystate(23, Ics, Line, Tlen+1, 0, Tlen); 473 | yystate(22, Ics, Line, Tlen, _, _) -> 474 | {0,Tlen,Ics,Line,22}; 475 | yystate(21, [96|Ics], Line, Tlen, Action, Alen) -> 476 | yystate(17, Ics, Line, Tlen+1, Action, Alen); 477 | yystate(21, [92|Ics], Line, Tlen, Action, Alen) -> 478 | yystate(13, Ics, Line, Tlen+1, Action, Alen); 479 | yystate(21, [10|Ics], Line, Tlen, Action, Alen) -> 480 | yystate(21, Ics, Line+1, Tlen+1, Action, Alen); 481 | yystate(21, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 482 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 483 | yystate(21, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 91 -> 484 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 485 | yystate(21, [C|Ics], Line, Tlen, Action, Alen) when C >= 93, C =< 95 -> 486 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 487 | yystate(21, [C|Ics], Line, Tlen, Action, Alen) when C >= 97 -> 488 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 489 | yystate(21, Ics, Line, Tlen, Action, Alen) -> 490 | {Action,Alen,Tlen,Ics,Line,21}; 491 | yystate(20, [94|Ics], Line, Tlen, Action, Alen) -> 492 | yystate(24, Ics, Line, Tlen+1, Action, Alen); 493 | yystate(20, [93|Ics], Line, Tlen, Action, Alen) -> 494 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 495 | yystate(20, [92|Ics], Line, Tlen, Action, Alen) -> 496 | yystate(20, Ics, Line, Tlen+1, Action, Alen); 497 | yystate(20, [39|Ics], Line, Tlen, Action, Alen) -> 498 | yystate(16, Ics, Line, Tlen+1, Action, Alen); 499 | yystate(20, [10|Ics], Line, Tlen, Action, Alen) -> 500 | yystate(8, Ics, Line+1, Tlen+1, Action, Alen); 501 | yystate(20, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 502 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 503 | yystate(20, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 38 -> 504 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 505 | yystate(20, [C|Ics], Line, Tlen, Action, Alen) when C >= 40, C =< 91 -> 506 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 507 | yystate(20, [C|Ics], Line, Tlen, Action, Alen) when C >= 95 -> 508 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 509 | yystate(20, Ics, Line, Tlen, Action, Alen) -> 510 | {Action,Alen,Tlen,Ics,Line,20}; 511 | yystate(19, [61|Ics], Line, Tlen, Action, Alen) -> 512 | yystate(23, Ics, Line, Tlen+1, Action, Alen); 513 | yystate(19, Ics, Line, Tlen, Action, Alen) -> 514 | {Action,Alen,Tlen,Ics,Line,19}; 515 | yystate(18, [62|Ics], Line, Tlen, _, _) -> 516 | yystate(23, Ics, Line, Tlen+1, 2, Tlen); 517 | yystate(18, [60|Ics], Line, Tlen, _, _) -> 518 | yystate(23, Ics, Line, Tlen+1, 2, Tlen); 519 | yystate(18, Ics, Line, Tlen, _, _) -> 520 | {2,Tlen,Ics,Line,18}; 521 | yystate(17, Ics, Line, Tlen, _, _) -> 522 | {5,Tlen,Ics,Line}; 523 | yystate(16, [92|Ics], Line, Tlen, _, _) -> 524 | yystate(20, Ics, Line, Tlen+1, 3, Tlen); 525 | yystate(16, [39|Ics], Line, Tlen, _, _) -> 526 | yystate(16, Ics, Line, Tlen+1, 3, Tlen); 527 | yystate(16, [10|Ics], Line, Tlen, _, _) -> 528 | yystate(8, Ics, Line+1, Tlen+1, 3, Tlen); 529 | yystate(16, [C|Ics], Line, Tlen, _, _) when C >= 0, C =< 9 -> 530 | yystate(8, Ics, Line, Tlen+1, 3, Tlen); 531 | yystate(16, [C|Ics], Line, Tlen, _, _) when C >= 11, C =< 38 -> 532 | yystate(8, Ics, Line, Tlen+1, 3, Tlen); 533 | yystate(16, [C|Ics], Line, Tlen, _, _) when C >= 40, C =< 91 -> 534 | yystate(8, Ics, Line, Tlen+1, 3, Tlen); 535 | yystate(16, [C|Ics], Line, Tlen, _, _) when C >= 93 -> 536 | yystate(8, Ics, Line, Tlen+1, 3, Tlen); 537 | yystate(16, Ics, Line, Tlen, _, _) -> 538 | {3,Tlen,Ics,Line,16}; 539 | yystate(15, [92|Ics], Line, Tlen, Action, Alen) -> 540 | yystate(3, Ics, Line, Tlen+1, Action, Alen); 541 | yystate(15, [34|Ics], Line, Tlen, Action, Alen) -> 542 | yystate(11, Ics, Line, Tlen+1, Action, Alen); 543 | yystate(15, [10|Ics], Line, Tlen, Action, Alen) -> 544 | yystate(15, Ics, Line+1, Tlen+1, Action, Alen); 545 | yystate(15, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 546 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 547 | yystate(15, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 33 -> 548 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 549 | yystate(15, [C|Ics], Line, Tlen, Action, Alen) when C >= 35, C =< 91 -> 550 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 551 | yystate(15, [C|Ics], Line, Tlen, Action, Alen) when C >= 93 -> 552 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 553 | yystate(15, Ics, Line, Tlen, Action, Alen) -> 554 | {Action,Alen,Tlen,Ics,Line,15}; 555 | yystate(14, [61|Ics], Line, Tlen, _, _) -> 556 | yystate(23, Ics, Line, Tlen+1, 0, Tlen); 557 | yystate(14, Ics, Line, Tlen, _, _) -> 558 | {0,Tlen,Ics,Line,14}; 559 | yystate(13, [96|Ics], Line, Tlen, Action, Alen) -> 560 | yystate(9, Ics, Line, Tlen+1, Action, Alen); 561 | yystate(13, [95|Ics], Line, Tlen, Action, Alen) -> 562 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 563 | yystate(13, [94|Ics], Line, Tlen, Action, Alen) -> 564 | yystate(5, Ics, Line, Tlen+1, Action, Alen); 565 | yystate(13, [93|Ics], Line, Tlen, Action, Alen) -> 566 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 567 | yystate(13, [92|Ics], Line, Tlen, Action, Alen) -> 568 | yystate(13, Ics, Line, Tlen+1, Action, Alen); 569 | yystate(13, [10|Ics], Line, Tlen, Action, Alen) -> 570 | yystate(21, Ics, Line+1, Tlen+1, Action, Alen); 571 | yystate(13, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 572 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 573 | yystate(13, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 91 -> 574 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 575 | yystate(13, [C|Ics], Line, Tlen, Action, Alen) when C >= 97 -> 576 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 577 | yystate(13, Ics, Line, Tlen, Action, Alen) -> 578 | {Action,Alen,Tlen,Ics,Line,13}; 579 | yystate(12, [39|Ics], Line, Tlen, _, _) -> 580 | yystate(8, Ics, Line, Tlen+1, 3, Tlen); 581 | yystate(12, Ics, Line, Tlen, _, _) -> 582 | {3,Tlen,Ics,Line,12}; 583 | yystate(11, [34|Ics], Line, Tlen, _, _) -> 584 | yystate(15, Ics, Line, Tlen+1, 4, Tlen); 585 | yystate(11, Ics, Line, Tlen, _, _) -> 586 | {4,Tlen,Ics,Line,11}; 587 | yystate(10, [95|Ics], Line, Tlen, _, _) -> 588 | yystate(10, Ics, Line, Tlen+1, 6, Tlen); 589 | yystate(10, [46|Ics], Line, Tlen, _, _) -> 590 | yystate(10, Ics, Line, Tlen+1, 6, Tlen); 591 | yystate(10, [C|Ics], Line, Tlen, _, _) when C >= 48, C =< 57 -> 592 | yystate(10, Ics, Line, Tlen+1, 6, Tlen); 593 | yystate(10, [C|Ics], Line, Tlen, _, _) when C >= 65, C =< 90 -> 594 | yystate(10, Ics, Line, Tlen+1, 6, Tlen); 595 | yystate(10, [C|Ics], Line, Tlen, _, _) when C >= 97, C =< 122 -> 596 | yystate(10, Ics, Line, Tlen+1, 6, Tlen); 597 | yystate(10, Ics, Line, Tlen, _, _) -> 598 | {6,Tlen,Ics,Line,10}; 599 | yystate(9, [96|Ics], Line, Tlen, _, _) -> 600 | yystate(17, Ics, Line, Tlen+1, 5, Tlen); 601 | yystate(9, [92|Ics], Line, Tlen, _, _) -> 602 | yystate(13, Ics, Line, Tlen+1, 5, Tlen); 603 | yystate(9, [10|Ics], Line, Tlen, _, _) -> 604 | yystate(21, Ics, Line+1, Tlen+1, 5, Tlen); 605 | yystate(9, [C|Ics], Line, Tlen, _, _) when C >= 0, C =< 9 -> 606 | yystate(21, Ics, Line, Tlen+1, 5, Tlen); 607 | yystate(9, [C|Ics], Line, Tlen, _, _) when C >= 11, C =< 91 -> 608 | yystate(21, Ics, Line, Tlen+1, 5, Tlen); 609 | yystate(9, [C|Ics], Line, Tlen, _, _) when C >= 93, C =< 95 -> 610 | yystate(21, Ics, Line, Tlen+1, 5, Tlen); 611 | yystate(9, [C|Ics], Line, Tlen, _, _) when C >= 97 -> 612 | yystate(21, Ics, Line, Tlen+1, 5, Tlen); 613 | yystate(9, Ics, Line, Tlen, _, _) -> 614 | {5,Tlen,Ics,Line,9}; 615 | yystate(8, [92|Ics], Line, Tlen, Action, Alen) -> 616 | yystate(20, Ics, Line, Tlen+1, Action, Alen); 617 | yystate(8, [39|Ics], Line, Tlen, Action, Alen) -> 618 | yystate(12, Ics, Line, Tlen+1, Action, Alen); 619 | yystate(8, [10|Ics], Line, Tlen, Action, Alen) -> 620 | yystate(8, Ics, Line+1, Tlen+1, Action, Alen); 621 | yystate(8, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 622 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 623 | yystate(8, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 38 -> 624 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 625 | yystate(8, [C|Ics], Line, Tlen, Action, Alen) when C >= 40, C =< 91 -> 626 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 627 | yystate(8, [C|Ics], Line, Tlen, Action, Alen) when C >= 93 -> 628 | yystate(8, Ics, Line, Tlen+1, Action, Alen); 629 | yystate(8, Ics, Line, Tlen, Action, Alen) -> 630 | {Action,Alen,Tlen,Ics,Line,8}; 631 | yystate(7, [92|Ics], Line, Tlen, _, _) -> 632 | yystate(3, Ics, Line, Tlen+1, 4, Tlen); 633 | yystate(7, [34|Ics], Line, Tlen, _, _) -> 634 | yystate(7, Ics, Line, Tlen+1, 4, Tlen); 635 | yystate(7, [10|Ics], Line, Tlen, _, _) -> 636 | yystate(15, Ics, Line+1, Tlen+1, 4, Tlen); 637 | yystate(7, [C|Ics], Line, Tlen, _, _) when C >= 0, C =< 9 -> 638 | yystate(15, Ics, Line, Tlen+1, 4, Tlen); 639 | yystate(7, [C|Ics], Line, Tlen, _, _) when C >= 11, C =< 33 -> 640 | yystate(15, Ics, Line, Tlen+1, 4, Tlen); 641 | yystate(7, [C|Ics], Line, Tlen, _, _) when C >= 35, C =< 91 -> 642 | yystate(15, Ics, Line, Tlen+1, 4, Tlen); 643 | yystate(7, [C|Ics], Line, Tlen, _, _) when C >= 93 -> 644 | yystate(15, Ics, Line, Tlen+1, 4, Tlen); 645 | yystate(7, Ics, Line, Tlen, _, _) -> 646 | {4,Tlen,Ics,Line,7}; 647 | yystate(6, [95|Ics], Line, Tlen, Action, Alen) -> 648 | yystate(10, Ics, Line, Tlen+1, Action, Alen); 649 | yystate(6, [C|Ics], Line, Tlen, Action, Alen) when C >= 65, C =< 90 -> 650 | yystate(10, Ics, Line, Tlen+1, Action, Alen); 651 | yystate(6, [C|Ics], Line, Tlen, Action, Alen) when C >= 97, C =< 122 -> 652 | yystate(10, Ics, Line, Tlen+1, Action, Alen); 653 | yystate(6, Ics, Line, Tlen, Action, Alen) -> 654 | {Action,Alen,Tlen,Ics,Line,6}; 655 | yystate(5, [96|Ics], Line, Tlen, Action, Alen) -> 656 | yystate(9, Ics, Line, Tlen+1, Action, Alen); 657 | yystate(5, [92|Ics], Line, Tlen, Action, Alen) -> 658 | yystate(13, Ics, Line, Tlen+1, Action, Alen); 659 | yystate(5, [10|Ics], Line, Tlen, Action, Alen) -> 660 | yystate(21, Ics, Line+1, Tlen+1, Action, Alen); 661 | yystate(5, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 662 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 663 | yystate(5, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 91 -> 664 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 665 | yystate(5, [C|Ics], Line, Tlen, Action, Alen) when C >= 93, C =< 95 -> 666 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 667 | yystate(5, [C|Ics], Line, Tlen, Action, Alen) when C >= 97 -> 668 | yystate(21, Ics, Line, Tlen+1, Action, Alen); 669 | yystate(5, Ics, Line, Tlen, Action, Alen) -> 670 | {Action,Alen,Tlen,Ics,Line,5}; 671 | yystate(4, [37|Ics], Line, Tlen, _, _) -> 672 | yystate(4, Ics, Line, Tlen+1, 8, Tlen); 673 | yystate(4, [10|Ics], Line, Tlen, _, _) -> 674 | yystate(27, Ics, Line+1, Tlen+1, 8, Tlen); 675 | yystate(4, [C|Ics], Line, Tlen, _, _) when C >= 0, C =< 9 -> 676 | yystate(4, Ics, Line, Tlen+1, 8, Tlen); 677 | yystate(4, [C|Ics], Line, Tlen, _, _) when C >= 11, C =< 32 -> 678 | yystate(4, Ics, Line, Tlen+1, 8, Tlen); 679 | yystate(4, [C|Ics], Line, Tlen, _, _) when C >= 33, C =< 36 -> 680 | yystate(4, Ics, Line, Tlen+1, 8, Tlen); 681 | yystate(4, [C|Ics], Line, Tlen, _, _) when C >= 38 -> 682 | yystate(4, Ics, Line, Tlen+1, 8, Tlen); 683 | yystate(4, Ics, Line, Tlen, _, _) -> 684 | {8,Tlen,Ics,Line,4}; 685 | yystate(3, [94|Ics], Line, Tlen, Action, Alen) -> 686 | yystate(0, Ics, Line, Tlen+1, Action, Alen); 687 | yystate(3, [93|Ics], Line, Tlen, Action, Alen) -> 688 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 689 | yystate(3, [92|Ics], Line, Tlen, Action, Alen) -> 690 | yystate(3, Ics, Line, Tlen+1, Action, Alen); 691 | yystate(3, [34|Ics], Line, Tlen, Action, Alen) -> 692 | yystate(7, Ics, Line, Tlen+1, Action, Alen); 693 | yystate(3, [10|Ics], Line, Tlen, Action, Alen) -> 694 | yystate(15, Ics, Line+1, Tlen+1, Action, Alen); 695 | yystate(3, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 696 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 697 | yystate(3, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 33 -> 698 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 699 | yystate(3, [C|Ics], Line, Tlen, Action, Alen) when C >= 35, C =< 91 -> 700 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 701 | yystate(3, [C|Ics], Line, Tlen, Action, Alen) when C >= 95 -> 702 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 703 | yystate(3, Ics, Line, Tlen, Action, Alen) -> 704 | {Action,Alen,Tlen,Ics,Line,3}; 705 | yystate(2, [64|Ics], Line, Tlen, Action, Alen) -> 706 | yystate(6, Ics, Line, Tlen+1, Action, Alen); 707 | yystate(2, Ics, Line, Tlen, Action, Alen) -> 708 | {Action,Alen,Tlen,Ics,Line,2}; 709 | yystate(1, [95|Ics], Line, Tlen, _, _) -> 710 | yystate(1, Ics, Line, Tlen+1, 7, Tlen); 711 | yystate(1, [C|Ics], Line, Tlen, _, _) when C >= 48, C =< 57 -> 712 | yystate(1, Ics, Line, Tlen+1, 7, Tlen); 713 | yystate(1, [C|Ics], Line, Tlen, _, _) when C >= 65, C =< 90 -> 714 | yystate(1, Ics, Line, Tlen+1, 7, Tlen); 715 | yystate(1, [C|Ics], Line, Tlen, _, _) when C >= 97, C =< 122 -> 716 | yystate(1, Ics, Line, Tlen+1, 7, Tlen); 717 | yystate(1, Ics, Line, Tlen, _, _) -> 718 | {7,Tlen,Ics,Line,1}; 719 | yystate(0, [92|Ics], Line, Tlen, Action, Alen) -> 720 | yystate(3, Ics, Line, Tlen+1, Action, Alen); 721 | yystate(0, [34|Ics], Line, Tlen, Action, Alen) -> 722 | yystate(7, Ics, Line, Tlen+1, Action, Alen); 723 | yystate(0, [10|Ics], Line, Tlen, Action, Alen) -> 724 | yystate(15, Ics, Line+1, Tlen+1, Action, Alen); 725 | yystate(0, [C|Ics], Line, Tlen, Action, Alen) when C >= 0, C =< 9 -> 726 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 727 | yystate(0, [C|Ics], Line, Tlen, Action, Alen) when C >= 11, C =< 33 -> 728 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 729 | yystate(0, [C|Ics], Line, Tlen, Action, Alen) when C >= 35, C =< 91 -> 730 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 731 | yystate(0, [C|Ics], Line, Tlen, Action, Alen) when C >= 93 -> 732 | yystate(15, Ics, Line, Tlen+1, Action, Alen); 733 | yystate(0, Ics, Line, Tlen, Action, Alen) -> 734 | {Action,Alen,Tlen,Ics,Line,0}; 735 | yystate(S, Ics, Line, Tlen, Action, Alen) -> 736 | {Action,Alen,Tlen,Ics,Line,S}. 737 | 738 | %% yyaction(Action, TokenLength, TokenChars, TokenLine) -> 739 | %% {token,Token} | {end_token, Token} | skip_token | {error,String}. 740 | %% Generated action function. 741 | 742 | yyaction(0, TokenLen, YYtcs, TokenLine) -> 743 | TokenChars = yypre(YYtcs, TokenLen), 744 | yyaction_0(TokenChars, TokenLine); 745 | yyaction(1, TokenLen, YYtcs, TokenLine) -> 746 | TokenChars = yypre(YYtcs, TokenLen), 747 | yyaction_1(TokenChars, TokenLine); 748 | yyaction(2, TokenLen, YYtcs, TokenLine) -> 749 | TokenChars = yypre(YYtcs, TokenLen), 750 | yyaction_2(TokenChars, TokenLine); 751 | yyaction(3, TokenLen, YYtcs, TokenLine) -> 752 | TokenChars = yypre(YYtcs, TokenLen), 753 | yyaction_3(TokenChars, TokenLen, TokenLine); 754 | yyaction(4, TokenLen, YYtcs, TokenLine) -> 755 | TokenChars = yypre(YYtcs, TokenLen), 756 | yyaction_4(TokenChars, TokenLen, TokenLine); 757 | yyaction(5, TokenLen, YYtcs, TokenLine) -> 758 | TokenChars = yypre(YYtcs, TokenLen), 759 | yyaction_5(TokenChars, TokenLen, TokenLine); 760 | yyaction(6, TokenLen, YYtcs, TokenLine) -> 761 | TokenChars = yypre(YYtcs, TokenLen), 762 | yyaction_6(TokenChars, TokenLine); 763 | yyaction(7, TokenLen, YYtcs, TokenLine) -> 764 | TokenChars = yypre(YYtcs, TokenLen), 765 | yyaction_7(TokenChars, TokenLine); 766 | yyaction(8, _, _, _) -> 767 | yyaction_8(); 768 | yyaction(_, _, _, _) -> error. 769 | 770 | -compile({inline,yyaction_0/2}). 771 | -file("src/sql92_scan.xrl", 14). 772 | yyaction_0(TokenChars, TokenLine) -> 773 | { token, { atom (TokenChars), TokenLine } } . 774 | 775 | -compile({inline,yyaction_1/2}). 776 | -file("src/sql92_scan.xrl", 15). 777 | yyaction_1(TokenChars, TokenLine) -> 778 | { token, { integer, TokenLine, integer (TokenChars) } } . 779 | 780 | -compile({inline,yyaction_2/2}). 781 | -file("src/sql92_scan.xrl", 16). 782 | yyaction_2(TokenChars, TokenLine) -> 783 | { token, { atom (TokenChars), TokenLine } } . 784 | 785 | -compile({inline,yyaction_3/3}). 786 | -file("src/sql92_scan.xrl", 17). 787 | yyaction_3(TokenChars, TokenLen, TokenLine) -> 788 | S = string_gen (strip (TokenChars, TokenLen)), 789 | { token, { string, TokenLine, bitstring (S) } } . 790 | 791 | -compile({inline,yyaction_4/3}). 792 | -file("src/sql92_scan.xrl", 19). 793 | yyaction_4(TokenChars, TokenLen, TokenLine) -> 794 | S = string_gen (strip (TokenChars, TokenLen)), 795 | { token, { string, TokenLine, bitstring (S) } } . 796 | 797 | -compile({inline,yyaction_5/3}). 798 | -file("src/sql92_scan.xrl", 21). 799 | yyaction_5(TokenChars, TokenLen, TokenLine) -> 800 | S = string_gen (strip (TokenChars, TokenLen)), 801 | { token, { name, TokenLine, bitstring (S) } } . 802 | 803 | -compile({inline,yyaction_6/2}). 804 | -file("src/sql92_scan.xrl", 23). 805 | yyaction_6(TokenChars, TokenLine) -> 806 | { token, { var, TokenLine, variable (TokenChars) } } . 807 | 808 | -compile({inline,yyaction_7/2}). 809 | -file("src/sql92_scan.xrl", 24). 810 | yyaction_7(TokenChars, TokenLine) -> 811 | check_reserved (TokenChars, TokenLine) . 812 | 813 | -compile({inline,yyaction_8/0}). 814 | -file("src/sql92_scan.xrl", 25). 815 | yyaction_8() -> 816 | skip_token . 817 | 818 | -file("/usr/local/Cellar/erlang/20.0/lib/erlang/lib/parsetools-2.1.5/include/leexinc.hrl", 309). 819 | -------------------------------------------------------------------------------- /src/sql92_scan.xrl: -------------------------------------------------------------------------------- 1 | %% @author Oleg Smirnov 2 | %% Derived under MIT license from https://github.com/master/mongosql 3 | %% @doc A grammar of a subset of SQL-92 4 | 5 | Definitions. 6 | 7 | D = [0-9] 8 | V = [A-Za-z_][A-Za-z0-9_\.]* 9 | L = [A-Za-z_][A-Za-z0-9_]* 10 | WS = ([\000-\s]|%.*) 11 | C = (<=|>=|>|<>|=<|=>|<|<>|!=) 12 | P = [-+*/(),;=.] 13 | 14 | Rules. 15 | 16 | {C} : {token, {atom(TokenChars), TokenLine}}. 17 | {D}+ : {token, {integer, TokenLine, integer(TokenChars)}}. 18 | {P} : {token, {atom(TokenChars), TokenLine}}. 19 | '(\\\^.|\\.|('')|[^'])*' : S = string_gen(strip(TokenChars, TokenLen)), 20 | {token, {string, TokenLine, bitstring(S)}}. 21 | "(\\\^.|\\.|("")|[^"])*" : S = string_gen(strip(TokenChars, TokenLen)), 22 | {token, {string, TokenLine, bitstring(S)}}. 23 | `(\\\^.|\\.|[^`])*` : S = string_gen(strip(TokenChars, TokenLen)), 24 | {token, {name, TokenLine, bitstring(S)}}. 25 | @@{V}+ : {token, {var, TokenLine, variable(TokenChars)}}. 26 | {L}+ : check_reserved(TokenChars, TokenLine). 27 | {WS}+ : skip_token. 28 | 29 | Erlang code. 30 | 31 | check_reserved(TokenChars, TokenLine) -> 32 | R = atom(string_gen(TokenChars)), 33 | case reserved_word(R) of 34 | true -> {token, {R, TokenLine}}; 35 | false -> {token, {name, TokenLine, bitstring(TokenChars)}} 36 | end. 37 | 38 | atom(TokenChars) -> list_to_atom(string:to_lower(TokenChars)). 39 | integer(TokenChars) -> list_to_integer(TokenChars). 40 | bitstring(TokenChars) -> list_to_bitstring(TokenChars). 41 | strip(TokenChars,TokenLen) -> lists:sublist(TokenChars, 2, TokenLen - 2). 42 | variable([_,_|TokenChars]) -> list_to_bitstring(TokenChars). 43 | 44 | reserved_word('all') -> true; 45 | reserved_word('and') -> true; 46 | reserved_word('asc') -> true; 47 | reserved_word('begin') -> true; 48 | reserved_word('between') -> true; 49 | reserved_word('by') -> true; 50 | reserved_word('commit') -> true; 51 | reserved_word('delete') -> true; 52 | reserved_word('desc') -> true; 53 | reserved_word('distinct') -> true; 54 | reserved_word('from') -> true; 55 | reserved_word('group') -> true; 56 | reserved_word('having') -> true; 57 | reserved_word('insert') -> true; 58 | reserved_word('in') -> true; 59 | reserved_word('into') -> true; 60 | reserved_word('is') -> true; 61 | reserved_word('like') -> true; 62 | reserved_word('limit') -> true; 63 | reserved_word('not') -> true; 64 | reserved_word('null') -> true; 65 | reserved_word('offset') -> true; 66 | reserved_word('or') -> true; 67 | reserved_word('order') -> true; 68 | reserved_word('rollback') -> true; 69 | reserved_word('select') -> true; 70 | reserved_word('describe') -> true; 71 | reserved_word('show') -> true; 72 | reserved_word('full') -> true; 73 | reserved_word('set') -> true; 74 | reserved_word('update') -> true; 75 | reserved_word('values') -> true; 76 | reserved_word('where') -> true; 77 | reserved_word('as') -> true; 78 | reserved_word('fields') -> true; 79 | reserved_word('index') -> true; 80 | reserved_word('create') -> true; 81 | reserved_word('table') -> true; 82 | reserved_word('tables') -> true; 83 | reserved_word('databases') -> true; 84 | reserved_word('variables') -> true; 85 | reserved_word('collation') -> true; 86 | reserved_word('collate') -> true; 87 | reserved_word('character') -> true; 88 | reserved_word('cast') -> true; 89 | reserved_word('use') -> true; 90 | reserved_word(_) -> false. 91 | 92 | string_gen([A,A|Cs]) when A == $'; A == $" -> 93 | [A|string_gen(Cs)]; 94 | string_gen([$\\|Cs]) -> 95 | string_escape(Cs); 96 | string_gen([C|Cs]) -> 97 | [C|string_gen(Cs)]; 98 | string_gen([]) -> []. 99 | 100 | string_escape([C|Cs]) -> 101 | case escape_char(C) of 102 | not_control -> [$\\, C|string_gen(Cs)]; 103 | Control -> [Control|string_gen(Cs)] 104 | end; 105 | string_escape([]) -> [$\\]. 106 | 107 | %% https://dev.mysql.com/doc/refman/5.7/en/string-literals.html#character-escape-sequences 108 | escape_char($0) -> $\0; 109 | escape_char($') -> $\'; 110 | escape_char($") -> $\"; 111 | escape_char($b) -> $\b; 112 | escape_char($n) -> $\n; 113 | escape_char($r) -> $\r; 114 | escape_char($t) -> $\t; 115 | escape_char($Z) -> 10#26; 116 | escape_char($\\) -> $\\; 117 | %% escape_char($%) -> $%; % TODO except like 118 | %% escape_char($_) -> $_; % TODO except like 119 | escape_char(_) -> not_control. 120 | -------------------------------------------------------------------------------- /src/sql_ets_api.erl: -------------------------------------------------------------------------------- 1 | -module(sql_ets_api). 2 | 3 | -export([authorize/4, connect_db/2, database/1, databases/1, tables/1, columns/2, terminate/2]). 4 | -export([select/3, insert/3, update/4, delete/3, fncall/3]). 5 | 6 | 7 | -record(state, { 8 | db 9 | }). 10 | 11 | authorize(Username, HashedPassword, Hash, _Env) -> 12 | case <<"login">> == Username andalso sqlapi:password_hash(<<"pass">>, Hash) == HashedPassword of 13 | true -> {ok, #state{}}; 14 | false -> {error, <<"Invalid login or password">>} 15 | end. 16 | 17 | 18 | connect_db(Database,State) -> 19 | {ok, State#state{db=Database}}. 20 | 21 | database(#state{db=DB}) -> DB. 22 | 23 | databases(#state{}) -> [<<"ets">>]. 24 | 25 | 26 | tables(#state{}) -> sqlapi_ram_table:tables(). 27 | 28 | columns(Table, #state{}) -> sqlapi_ram_table:columns(Table). 29 | 30 | terminate(_,_State) -> ok. 31 | 32 | select(Table, Filter, _State) -> sqlapi_ram_table:select(Table, Filter). 33 | 34 | insert(Table, Values, _State) -> sqlapi_ram_table:insert(Table, Values). 35 | 36 | update(Table, Values, Conditions, _State) -> sqlapi_ram_table:update(Table, Values, Conditions). 37 | 38 | delete(Table, Conditions, _State) -> sqlapi_ram_table:delete(Table, Conditions). 39 | 40 | % SELECT create_ram_table('sessions','session_id:string','user_id:integer','last_seen_at:integer'); 41 | fncall(<<"create_ram_table">>, [TableName | ColumnList], _State) -> 42 | Columns = lists:map(fun(C) -> 43 | [Column, Type] = binary:split(C,<<":">>), 44 | {binary_to_atom(Column,latin1),binary_to_atom(Type,latin1)} 45 | end, ColumnList), 46 | Table = binary_to_atom(TableName,latin1), 47 | sqlapi_ram_table:create_table(Table, Columns), 48 | {ok, #{status => ok}}; 49 | 50 | fncall(<<"drop_ram_table">>, [TableName], _State) -> 51 | sqlapi_ram_table:drop_table(binary_to_atom(TableName,latin1)), 52 | {ok, #{status => ok}}. 53 | 54 | -------------------------------------------------------------------------------- /src/sqlapi.app.src: -------------------------------------------------------------------------------- 1 | {application, sqlapi, 2 | [ 3 | {description, ""}, 4 | {vsn, "1.0.0"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {mod, { sqlapi_app, []}}, 11 | {env, []} 12 | ]}. 13 | -------------------------------------------------------------------------------- /src/sqlapi.erl: -------------------------------------------------------------------------------- 1 | -module(sqlapi). 2 | 3 | -include("../include/sqlapi.hrl"). 4 | -include("myproto.hrl"). 5 | 6 | 7 | -export([start_server/1, stop_server/1, existing_port/1, load_config/1]). 8 | 9 | -export([password_hash/2, execute/3, terminate/2]). 10 | -export([create_temporary_table/2, drop_temporary_table/1]). 11 | 12 | 13 | -export([notify/2]). 14 | 15 | 16 | -export([apply_sql_filter/2, arith/3]). 17 | -export([mk_cond/1, filter/2, sqlite_flt/1, find_constraint/2]). 18 | 19 | -define(ERR_WRONG_PASS, {error, <<"Password incorrect!">>}). 20 | -define(ERR_WRONG_USER, {error, <<"No such user!">>}). 21 | -define(ERR_LOGIN_DISABLED, {error, <<"Login disabled">>}). 22 | -define(ERR_INFO(Code, Desc), #response{status=?STATUS_ERR, error_code=Code, info=Desc}). 23 | 24 | -define(field(Key,Dict), proplists:get_value(Key,Dict)). 25 | 26 | -define(COUNT_COL_NAME, ' COUNT '). 27 | 28 | load_config(#{handler := _} = Env0) -> 29 | Env = maps:merge(#{listener_name => sqlapi_ranch}, Env0), 30 | 31 | MysqlPort = maps:get(port, Env, undefined), 32 | OldMysql = existing_port(Env), 33 | 34 | if 35 | OldMysql == MysqlPort -> ok; 36 | OldMysql == undefined andalso MysqlPort =/= undefined -> 37 | start_server(Env); 38 | MysqlPort =/= undefined andalso MysqlPort =/= OldMysql andalso OldMysql =/= undefined -> 39 | stop_server(Env), 40 | start_server(Env); 41 | MysqlPort == undefined -> 42 | stop_server(Env) 43 | end. 44 | 45 | 46 | existing_port(#{listener_name := Ref, trivial := true}) -> 47 | case whereis(Ref) of 48 | undefined -> undefined; 49 | Pid -> 50 | case process_info(Pid, dictionary) of 51 | {dictionary, Dict} -> proplists:get_value('$my_listener_port',Dict); 52 | _ -> undefined 53 | end 54 | end; 55 | 56 | existing_port(#{listener_name := Ref}) -> 57 | existing_port(Ref); 58 | 59 | existing_port(Ref) when is_atom(Ref) -> 60 | Supervisors = supervisor:which_children(ranch_sup), 61 | case lists:keyfind({ranch_listener_sup,Ref}, 1, Supervisors) of 62 | {_, Pid, _, _} when is_pid(Pid) -> 63 | case process_info(Pid) of 64 | undefined -> undefined; 65 | _ -> ranch:get_port(Ref) 66 | end; 67 | _ -> 68 | undefined 69 | end. 70 | 71 | start_server(#{listener_name := Name, port := Port, handler := Handler, trivial := true} = Env) -> 72 | my_ranch_worker:start_trivial_server(Port, Name, Handler, Env); 73 | 74 | start_server(#{listener_name := Name, port := HostPort, handler := Handler} = Env) -> 75 | {Host,Port} = if 76 | is_number(HostPort) -> {[], HostPort}; 77 | is_binary(HostPort) -> 78 | [H,P] = binary:split(HostPort, <<":">>), 79 | {ok, Ip} = inet_parse:address(binary_to_list(H)), 80 | {[{ip,Ip}],binary_to_integer(P)} 81 | end, 82 | 83 | case gen_tcp:listen(Port, [binary, {reuseaddr, true}, {active, false}, {backlog, 4096}] ++ Host) of 84 | {ok, LSock} -> 85 | ranch:start_listener(Name, 5, ranch_tcp, [{socket, LSock},{max_connections,300}], my_ranch_worker, [Handler, Env]); 86 | {error, _}=Err -> 87 | Err 88 | end. 89 | 90 | 91 | stop_server(#{trivial := true, listener_name := Name}) -> 92 | case whereis(Name) of 93 | undefined -> ok; 94 | Pid -> erlang:exit(Pid, shutdown) 95 | end; 96 | 97 | stop_server(#{listener_name := Name}) -> 98 | stop_server(Name); 99 | 100 | stop_server(Name) when is_atom(Name) -> 101 | my_ranch_worker:stop_server(Name). 102 | 103 | 104 | 105 | notify(Event, Metadata) -> 106 | case application:get_env(sqlapi, error_handler) of 107 | {ok, {M,F}} -> M:F(Event, Metadata); 108 | undefined -> error_logger:error_msg("~p: ~p",[Event,Metadata]) 109 | end. 110 | 111 | 112 | 113 | 114 | password_hash(PlaintextPassword, Salt) -> 115 | my_protocol:hash_password(PlaintextPassword, Salt). 116 | 117 | 118 | 119 | 120 | create_temporary_table(TableName, TableSpec) -> 121 | Reply = sqlapi_ram_table:create_table(TableName, TableSpec), 122 | Reply. 123 | 124 | drop_temporary_table(TableName) -> 125 | Reply = sqlapi_ram_table:drop_table(TableName), 126 | Reply. 127 | 128 | 129 | 130 | 131 | 132 | %% filter, aggregate, order and limit rows after select query 133 | apply_sql_filter(Rows, #sql_filter{conditions = Conditions} = Filter) when is_list(Rows) -> 134 | Rows1 = lists:map(fun 135 | (R) when is_list(R) -> R; 136 | (R) -> maps:to_list(R) 137 | end, Rows), 138 | Rows2 = [[{to_a(C), V} || {C, V} <- Row] || Row <- Rows1], 139 | Cond = mk_cond(Conditions), 140 | Rows3 = lists:foldl(fun 141 | (_S, {error,Err}) -> {error,Err}; 142 | (S, Acc) -> 143 | case filter(Cond,S) of 144 | true -> [S|Acc]; 145 | false -> Acc; 146 | Err -> Err 147 | end 148 | end, [], Rows2), 149 | 150 | if not is_list(Rows3) -> 151 | {error, 1036, <<"such filtering is not supported yet">>}; 152 | true -> 153 | Rows4 = aggregate_by_sql(Rows3, Filter), 154 | Rows5 = order_by_sql(Rows4, Filter), 155 | Rows6 = limit_by_sql(Rows5, Filter), 156 | Rows7 = filter_columns_to_map(Rows6, Filter), 157 | Rows7 158 | end; 159 | apply_sql_filter(Error, _Filter) when is_tuple(Error) -> 160 | Error. 161 | 162 | 163 | aggregate_by_sql({error,_,_} = Err, _) -> 164 | Err; 165 | 166 | aggregate_by_sql(Rows, #sql_filter{group = Group, columns = Columns}) -> 167 | Groupped = lists:foldl(fun(Row, Acc) -> 168 | Key = if Group == undefined -> undefined; 169 | true -> [proplists:lookup(C, Row) || C <- Group] 170 | end, 171 | RowsForKey = maps:get(Key, Acc, []), 172 | Acc#{Key => [Row| RowsForKey]} 173 | end, #{}, Rows), 174 | NeedCount = lists:member(?COUNT_COL_NAME, Columns), 175 | Rows1 = lists:flatmap(fun 176 | ({undefined, GroupRows}) when NeedCount -> add_count(GroupRows, length(GroupRows)); 177 | ({undefined, GroupRows}) -> GroupRows; 178 | ({_, GroupRows}) when NeedCount -> add_count([hd(GroupRows)], length(GroupRows)); 179 | ({_, GroupRows}) -> [hd(GroupRows)] 180 | end, maps:to_list(Groupped)), 181 | Rows1. 182 | 183 | 184 | add_count([Row| Rows], Count) -> 185 | Row1 = [{?COUNT_COL_NAME, Count}| Row], 186 | [Row1| add_count(Rows, Count)]; 187 | add_count([], _) -> []. 188 | 189 | 190 | 191 | order_by_sql(Rows, #sql_filter{order = Order, table_columns = TableColumns}) 192 | when length(Rows) > 0, Order /= undefined -> 193 | Keys = proplists:get_keys(TableColumns), 194 | KeyOrder = [{to_a(Key), Sort} || #order{key = Key, sort = Sort} <- Order, 195 | lists:member(to_a(Key), Keys)], 196 | if length(KeyOrder) /= length(Order) -> {error, 1036, <<"unknown column in order clause">>}; 197 | true -> 198 | Rows1 = lists:sort(fun(R1, R2) -> 199 | Pred = lists:foldl(fun 200 | ({Key, Sort}, undefined) -> 201 | Val1 = proplists:get_value(Key, R1, undefined), 202 | Val2 = proplists:get_value(Key, R2, undefined), 203 | case Sort of 204 | _ when Val1 == Val2 -> undefined; 205 | asc when Val1 == undefined -> true; % null first mysql default 206 | asc when Val2 == undefined -> false; 207 | asc -> Val1 < Val2; 208 | desc when Val1 == undefined -> false; % null last mysql default 209 | desc when Val2 == undefined -> true; 210 | desc -> Val1 > Val2 211 | end; 212 | (_, Acc) -> Acc 213 | end, undefined, KeyOrder), 214 | Pred == undefined orelse Pred 215 | end, Rows), 216 | Rows1 217 | end; 218 | order_by_sql(Rows, _Filter) -> 219 | Rows. 220 | 221 | 222 | 223 | %% limit/offset rows, convert to maps 224 | limit_by_sql(Rows, #sql_filter{limit = Limit, offset = Offset}) when 225 | is_list(Rows) andalso (Limit =/= undefined orelse Offset =/= undefined) -> 226 | 227 | case Limit of 228 | undefined -> 229 | Rows; 230 | Limit -> 231 | case Offset of 232 | undefined -> lists:sublist(Rows, 1, Limit); 233 | Offset when Offset < length(Rows) -> lists:sublist(Rows, Offset+1, Limit); 234 | _ -> [] 235 | end 236 | end; 237 | 238 | limit_by_sql(Rows, _) -> 239 | Rows. 240 | 241 | 242 | filter_columns_to_map(Rows, #sql_filter{columns = Columns}) when is_list(Rows) -> 243 | Rows1 = lists:map(fun 244 | (Row) when Columns == [] -> 245 | maps:from_list(Row); 246 | (Row) -> 247 | Row1 = [{C, proplists:get_value(C, Row)} || C <- Columns], 248 | maps:from_list(Row1) 249 | end, Rows), 250 | Rows1; 251 | filter_columns_to_map(Err, _) when is_tuple(Err) -> 252 | Err. 253 | 254 | 255 | 256 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 257 | %%% Erlang-side condition transformation %%% 258 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 259 | filter({nexo_or, Cond1, Cond2}, Item) -> filter(Cond1, Item) orelse filter(Cond2, Item); 260 | filter({nexo_and,Cond1, Cond2}, Item) -> filter(Cond1, Item) andalso filter(Cond2, Item); 261 | filter({eq, Arg1, Arg2}, Item) -> arith(eq, Arg1(Item), Arg2(Item)); 262 | filter({neq, Arg1, Arg2}, Item) -> not arith(eq, Arg1(Item), Arg2(Item)); 263 | filter({lt, Arg1, Arg2}, Item) -> arith(lt, Arg1(Item), Arg2(Item)); 264 | filter({gt, Arg1, Arg2}, Item) -> arith(gt, Arg1(Item), Arg2(Item)); 265 | filter({lte, Arg1, Arg2}, Item) -> arith(lte, Arg1(Item), Arg2(Item)); 266 | filter({gte, Arg1, Arg2}, Item) -> arith(gte, Arg1(Item), Arg2(Item)); 267 | filter({is, Arg1, Arg2}, Item) -> arith(is, Arg1(Item), Arg2(Item)); 268 | filter({is_not, Arg1, Arg2}, Item) -> not arith(is, Arg1(Item), Arg2(Item)); 269 | filter({not_in, Arg1, Arg2}, Item) -> not lists:member(Arg1(Item), Arg2(Item)); 270 | filter({in, Arg1, Arg2}, Item) -> V = Arg1(Item), lists:any(fun(Test) -> arith(eq, V, Test) end, Arg2(Item)); 271 | filter(no_cond,_) -> true; 272 | filter({like, Arg1, Arg2}, Item) -> like(Arg1(Item), Arg2); 273 | filter({not_like, Arg1, Arg2}, Item) -> not_like(Arg1(Item), Arg2); 274 | filter(_,_) -> {error, not_supported}. 275 | 276 | 277 | arith(Op, V1, V2) when Op == eq; Op == is -> 278 | IsFalse = fun(V) -> 279 | lists:member(V, [false, 0] ++ if Op == is -> [undefined, null]; true -> [] end) 280 | end, 281 | if V1 == true orelse V1 == 1 -> not IsFalse(V2); 282 | V1 == V2 -> true; 283 | true -> IsFalse(V1) andalso IsFalse(V2) 284 | end; 285 | arith(Op, V1, V2) when is_number(V1), is_number(V2) -> 286 | case Op of 287 | lt -> V1 < V2; 288 | gt -> V1 > V2; 289 | lte -> V1 =< V2; 290 | gte -> V1 >= V2 291 | end; 292 | arith(_Op, _V1, _V2) -> 293 | false. 294 | 295 | 296 | like(Value, Pattern) when is_binary(Value), is_binary(Pattern) -> 297 | like0(unicode:characters_to_list(Pattern), unicode:characters_to_list(Value)); 298 | like(undefined, Pattern) -> like(<<>>, Pattern); 299 | like(_,_) -> {error, not_supported}. 300 | 301 | not_like(Value, Pattern) -> 302 | case like(Value, Pattern) of 303 | true -> false; 304 | false -> true; 305 | Error -> Error 306 | end. 307 | 308 | like0(P, V) when P == V -> true; 309 | 310 | %% check \_, \%, \\ 311 | like0([$\\, $_ | P], [$_| V]) -> like0(P, V); 312 | like0([$\\, $_ | _], _) -> false; 313 | like0([$\\, $% | P], [$%| V]) -> like0(P, V); 314 | like0([$\\, $% | _], _) -> false; 315 | like0([$\\, $\\ | P], [$\\| V]) -> like0(P, V); 316 | like0([$\\, $\\ | _], _) -> false; 317 | 318 | %% '_': skip symbol 319 | like0([$_| P], [_| V]) -> like0(P, V); 320 | 321 | %% '%': skip symbols, check further pattern 322 | like0([$%| []], _) -> true; 323 | like0([$%| P], V) -> 324 | case like0(P, V) of 325 | false when V == [] -> false; 326 | false -> like0([$%| P], tl(V)); % keep pattern and continue until end of the string 327 | true -> true 328 | end; 329 | 330 | like0([S| P], [S| V]) -> like0(P, V); 331 | 332 | like0(_, _) -> false. 333 | 334 | 335 | mk_cond(#condition{nexo=Op, op1=Arg1, op2=Arg2}) -> {Op, mk_cond(Arg1), mk_cond(Arg2)}; 336 | mk_cond(undefined) -> no_cond; 337 | mk_cond(#key{name = <<"false">>, table=undefined}) -> fun(_) -> false end; 338 | mk_cond(#key{name = <<"true">>, table=undefined}) -> fun(_) -> true end; 339 | mk_cond(#key{name=Field}) -> fun 340 | (Item) when is_list(Item) -> proplists:get_value(binary_to_existing_atom(Field,utf8), Item); 341 | (Item) when is_map(Item) -> maps:get(binary_to_existing_atom(Field,utf8), Item, undefined) 342 | end; 343 | mk_cond(#value{value=Value}) -> fun(_) -> Value end; 344 | mk_cond(#subquery{subquery=Set}) -> fun(_) -> Set end; 345 | mk_cond(Arg) -> Arg. 346 | 347 | % unpack_filter_conditions({condition, nexo_and, Cond1, Cond2}) -> 348 | % unpack_filter_conditions(Cond1) ++ unpack_filter_conditions(Cond2); 349 | 350 | % unpack_filter_conditions({condition, Op, {key, Key, _, _}, {value, _, Value}}) when 351 | % Op == eq; Op == gt; Op == gte; Op == lt; Op == lte -> 352 | % [{{Key,Op},Value}]. 353 | 354 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 355 | %%% Extraction single condition from tree %%% 356 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 357 | find_constraint([], _) -> not_found; 358 | find_constraint([C|Rest], Cond) -> 359 | case find_constraint(C, Cond) of 360 | not_found -> find_constraint(Rest, Cond); 361 | Value -> Value 362 | end; 363 | 364 | %% single constraint search 365 | find_constraint({Op, Key}, {condition, Op, Value, #key{name=Key}}) -> Value; 366 | find_constraint({Op, Key}, {condition, Op, #key{name=Key}, Value}) -> Value; 367 | find_constraint(Part, {condition, _, Arg1, Arg2}) -> 368 | case find_constraint(Part, Arg1) of 369 | not_found -> find_constraint(Part, Arg2); 370 | Value -> Value 371 | end; 372 | 373 | find_constraint(_,_) -> 374 | not_found. 375 | 376 | 377 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 378 | %%% SQLite condition transformation %%% 379 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 380 | sqlite_flt(undefined) -> {"",[]}; 381 | sqlite_flt(Cond) -> 382 | {C, Par} = sqlite_flt(Cond, []), 383 | {" WHERE " ++ C, Par}. 384 | 385 | sqlite_flt({key,Arg,_,_},Par) -> {binary_to_existing_atom(Arg,utf8), Par}; 386 | sqlite_flt({value,_,Arg},Par) -> {"?", [Arg|Par]}; 387 | sqlite_flt({subquery,_,Set},Par) -> {["(?", [",?"||_<-tl(Set)], ")"], [Set|Par]}; 388 | sqlite_flt({condition, Op, Arg1, Arg2}, Par) -> 389 | %% reverse argument traverse order to maintain resulting parameter order 390 | {A2, P2} = sqlite_flt(Arg2,Par), 391 | {A1, P1} = sqlite_flt(Arg1,P2), 392 | Query = lists:flatten(io_lib:format("(~s ~s ~s)", [A1,sqlite_op(Op),A2])), 393 | {Query,P1}. 394 | 395 | sqlite_op(nexo_and) -> "and"; 396 | sqlite_op(nexo_or) -> "or"; 397 | sqlite_op(eq) -> "="; 398 | sqlite_op(lt) -> "<"; 399 | sqlite_op(gt) -> ">"; 400 | sqlite_op(lte) -> "<="; 401 | sqlite_op(gte) -> ">="; 402 | sqlite_op(in) -> "in". 403 | 404 | 405 | 406 | 407 | 408 | 409 | % $$\ $$\ 410 | % $$ | \__| 411 | % $$ | $$$$$$\ $$$$$$\ $$\ $$$$$$$\ 412 | % $$ | $$ __$$\ $$ __$$\ $$ |$$ _____| 413 | % $$ | $$ / $$ |$$ / $$ |$$ |$$ / 414 | % $$ | $$ | $$ |$$ | $$ |$$ |$$ | 415 | % $$$$$$$$\\$$$$$$ |\$$$$$$$ |$$ |\$$$$$$$\ 416 | % \________|\______/ \____$$ |\__| \_______| 417 | % $$\ $$ | 418 | % \$$$$$$ | 419 | % \______/ 420 | 421 | 422 | execute(#request{text = Text} = Request, Handler, State) -> 423 | try execute0(Request, Handler, State) of 424 | {reply, #response{}, _} = Reply -> 425 | Reply 426 | catch 427 | _C:E -> 428 | ST = erlang:get_stacktrace(), 429 | notify(sql_handler_error,[{module,?MODULE},{line,?LINE},{pid,self()},{application,sqlapi}, 430 | {query, Text}, {error, E}, {stacktrace, iolist_to_binary(io_lib:format("~p",[ST]))}]), 431 | {reply, #response{status=?STATUS_ERR, error_code=500, info= <<"Internal server error">>}, State} 432 | end. 433 | 434 | 435 | 436 | execute0(#request{info = #select{tables = [#table{name = Table}]} = Select}, Handler, State) when is_binary(Table) -> 437 | 438 | case Handler:columns(Table, State) of 439 | {error, no_table} -> 440 | {reply, #response{status=?STATUS_ERR, error_code=1146, info = <<"Table '",Table/binary,"' doesn't exist">>}, State}; 441 | 442 | TableColumns when is_list(TableColumns) -> 443 | Filter = build_sql_filter(Select, TableColumns), 444 | Columns = Filter#sql_filter.columns, 445 | case Handler:select(Table, Filter, State) of 446 | {error, Code, Desc} -> 447 | {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; 448 | 449 | Rows when is_list(Rows) -> 450 | ResponseColumns = [case lists:keyfind(Name,1,TableColumns) of 451 | _ when Name == ?COUNT_COL_NAME -> {<<"COUNT">>, integer}; 452 | false -> {Name, string}; 453 | Col -> Col 454 | end || Name <- Columns], 455 | Response = { response_columns(ResponseColumns), [response_row(Row, Columns) || Row <- Rows]}, 456 | Reply = #response{status=?STATUS_OK, info = Response}, 457 | {reply, Reply, State} 458 | end 459 | end; 460 | 461 | 462 | execute0(#request{info = #insert{table = #table{name = Table}, values = ValuesSpec}}, Handler, State) -> 463 | Values = lists:map(fun(Row) -> 464 | maps:from_list(lists:map(fun 465 | (#set{key = K, value = #value{value = V}}) -> {K,V}; 466 | (#set{key = K, value = #key{name = <<"true">>}}) -> {K,true}; 467 | (#set{key = K, value = #key{name = <<"TRUE">>}}) -> {K,true}; 468 | (#set{key = K, value = #key{name = <<"false">>}}) -> {K,false}; 469 | (#set{key = K, value = #key{name = <<"FALSE">>}}) -> {K,false} 470 | end, Row)) 471 | end, ValuesSpec), 472 | case Handler:insert(Table, Values, State) of 473 | {error, Code, Desc} -> 474 | {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; 475 | {ok, #{status := Status} = Reply} -> 476 | StatusCode = case Status of 477 | ok -> ?STATUS_OK 478 | end, 479 | AffectedRows = maps:get(affected_rows, Reply, 1), 480 | Id = maps:get(id, Reply, 0), 481 | Info = maps:get(info, Reply, <<>>), 482 | Warnings = maps:get(warnings, Reply, 0), 483 | {reply, #response{status=StatusCode, affected_rows = AffectedRows, last_insert_id = Id, status_flags = 0, warnings = Warnings, info = Info}, State}; 484 | {ok, Id} when is_integer(Id) -> 485 | {reply, #response{status=?STATUS_OK, affected_rows = 1, last_insert_id = Id, status_flags = 0, warnings = 0, info = <<>>}, State} 486 | end; 487 | 488 | 489 | 490 | execute0(#request{info= #update{table = #table{name=Table}, set=ValuesSpec, conditions=Conditions}}, Handler, State) -> 491 | Values = maps:from_list([{K,V} || #set{key = K, value = #value{value = V}} <- ValuesSpec]), 492 | case Handler:update(Table, Values, Conditions, State) of 493 | {error, Code, Desc} -> 494 | {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; 495 | {ok, #{status := Status} = Reply} -> 496 | StatusCode = case Status of 497 | ok -> ?STATUS_OK 498 | end, 499 | AffectedRows = maps:get(affected_rows, Reply, 1), 500 | Id = maps:get(id, Reply, 0), 501 | Info = maps:get(info, Reply, <<>>), 502 | Warnings = maps:get(warnings, Reply, 0), 503 | {reply, #response{status=StatusCode, affected_rows = AffectedRows, last_insert_id = Id, status_flags = 0, warnings = Warnings, info = Info}, State}; 504 | {ok, Id} when is_integer(Id) -> 505 | {reply, #response{status=?STATUS_OK, affected_rows = 1, last_insert_id = Id, status_flags = 0, warnings = 0, info = <<>>}, State} 506 | end; 507 | 508 | 509 | 510 | execute0(#request{info = #delete{table = #table{name = Table}, conditions = Conditions}}, Handler, State) -> 511 | case Handler:delete(Table, Conditions, State) of 512 | {error, Code, Desc} -> 513 | {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; 514 | {ok, #{status := Status} = Reply} -> 515 | StatusCode = case Status of 516 | ok -> ?STATUS_OK 517 | end, 518 | AffectedRows = maps:get(affected_rows, Reply, 1), 519 | Id = maps:get(id, Reply, 0), 520 | Info = maps:get(info, Reply, <<>>), 521 | Warnings = maps:get(warnings, Reply, 0), 522 | {reply, #response{status=StatusCode, affected_rows = AffectedRows, last_insert_id = Id, status_flags = 0, warnings = Warnings, info = Info}, State}; 523 | {ok, Id} when is_integer(Id) -> 524 | {reply, #response{status=?STATUS_OK, affected_rows = 1, last_insert_id = Id, status_flags = 0, warnings = 0, info = <<>>}, State} 525 | end; 526 | 527 | execute0(#request{info = #select{params = [#function{name = Name, params = Params0}]}}, Handler, State) -> 528 | Params = [V || #value{value=V} <- Params0], 529 | case Handler:fncall(Name, Params, State) of 530 | {error, Code, Desc} -> 531 | {reply, #response{status=?STATUS_ERR, error_code=Code, info=Desc}, State}; 532 | {ok, Columns, Rows} -> 533 | Columns1 = [C || {C,_} <- Columns], 534 | Response = {response_columns(Columns), [response_row(Row, Columns1) || Row <- Rows]}, 535 | % Response = {response_columns(Columns), Rows}, 536 | {reply, #response{status=?STATUS_OK, info = Response}, State}; 537 | {ok, #{status := Status} = Reply} -> 538 | StatusCode = case Status of 539 | ok -> ?STATUS_OK 540 | end, 541 | AffectedRows = maps:get(affected_rows, Reply, 1), 542 | Id = maps:get(id, Reply, 0), 543 | Info = maps:get(info, Reply, <<>>), 544 | Warnings = maps:get(warnings, Reply, 0), 545 | {reply, #response{status=StatusCode, affected_rows = AffectedRows, last_insert_id = Id, status_flags = 0, warnings = Warnings, info = Info}, State}; 546 | {ok, Id} when is_integer(Id) -> 547 | {reply, #response{status=?STATUS_OK, affected_rows = 1, last_insert_id = Id, status_flags = 0, warnings = 0, info = <<>>}, State}; 548 | ok -> 549 | {reply, #response{status=?STATUS_OK}, State} 550 | end; 551 | 552 | execute0(#request{text = Text, info = {error,{_,sql92_parser,Desc}}}, _Handler, State) -> 553 | Description = iolist_to_binary(["Invalid query: ",Desc]), 554 | notify(sql_error,[{module,?MODULE},{line,?LINE},{pid,self()},{application,sqlapi},{query, Text}, {error, bad_query}]), 555 | {reply, #response{status=?STATUS_ERR, error_code=1065, info= Description}, State}; 556 | 557 | execute0(#request{text = Text}, _Handler, State) -> 558 | notify(sql_error,[{module,?MODULE},{line,?LINE},{pid,self()},{application,sqlapi},{query, Text}, {error, unknown_query}]), 559 | {reply, #response{status=?STATUS_ERR, error_code=500, info= <<"Internal server error">>}, State}. 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | build_sql_filter(#select{params = Params, conditions = Conditions0, order = Order, group = Group0, 568 | limit = Limit, offset = Offset}, TableColumns) when is_list(TableColumns) -> 569 | 570 | Conditions = normalize_condition_types(TableColumns, Conditions0), 571 | 572 | Columns = lists:flatmap(fun 573 | (#all{}) -> [N || {N,_} <- TableColumns]; 574 | (#key{name = Name}) -> [binary_to_existing_atom(Name,latin1)]; 575 | (#function{name = N}) when N == <<"COUNT">> orelse N == <<"count">> -> [?COUNT_COL_NAME]; 576 | (_) -> [] % other aggregate, etc 577 | end, Params), 578 | 579 | Group = if Group0 == undefined -> undefined; 580 | true -> [binary_to_existing_atom(C,latin1) || C <- Group0] 581 | end, 582 | 583 | #sql_filter{ 584 | conditions = Conditions, 585 | columns = Columns, 586 | table_columns = TableColumns, 587 | order = Order, 588 | group = Group, 589 | limit = Limit, 590 | offset = Offset}. 591 | 592 | 593 | normalize_condition_types(Columns, #condition{nexo = OrAnd, op1 = Op1, op2 = Op2}) when OrAnd == nexo_and; OrAnd == nexo_or -> 594 | #condition{nexo = OrAnd, 595 | op1 = normalize_condition_types(Columns, Op1), 596 | op2 = normalize_condition_types(Columns, Op2) 597 | }; 598 | 599 | normalize_condition_types(Columns, #condition{nexo = C, op1 = #key{name = Name} = K, op2 = #key{name = Bool}} = Cond) 600 | when Bool == <<"true">>; Bool == <<"false">>; Bool == <<"TRUE">>; Bool == <<"FALSE">> -> 601 | Column = binary_to_existing_atom(Name,latin1), 602 | case proplists:get_value(Column, Columns) of 603 | boolean when Bool == <<"true">>; Bool == <<"TRUE">> -> #condition{nexo = C, op1 = K, op2 = #value{value = true}}; 604 | boolean when Bool == <<"FALSE">>; Bool == <<"FALSE">> -> #condition{nexo = C, op1 = K, op2 = #value{value = false}}; 605 | _ -> Cond 606 | end; 607 | 608 | 609 | normalize_condition_types(Columns, #condition{nexo = C, op1 = #key{name = Name} = K, op2 = #value{value = V}} = Cond) when V == 1; V == 0-> 610 | Column = binary_to_existing_atom(Name,latin1), 611 | case proplists:get_value(Column, Columns) of 612 | boolean when V == 1 -> #condition{nexo = C, op1 = K, op2 = #value{value = true}}; 613 | boolean when V == 0 -> #condition{nexo = C, op1 = K, op2 = #value{value = false}}; 614 | _ -> Cond 615 | end; 616 | 617 | normalize_condition_types(_Columns, Cond) -> 618 | Cond. 619 | 620 | 621 | 622 | 623 | response_row(Row, Columns) when is_map(Row) -> 624 | [maps:get(Column, Row, undefined) || Column <- Columns]; 625 | 626 | response_row(Row, Columns) when is_list(Row) -> 627 | [proplists:get_value(Column, Row) || Column <- Columns]. 628 | 629 | 630 | response_columns(Columns) -> 631 | lists:map(fun 632 | ({Name,string}) -> #column{name = to_b(Name), type = ?TYPE_VAR_STRING, length = 20, org_name = to_b(Name)}; 633 | ({Name,boolean}) -> #column{name = to_b(Name), type = ?TYPE_TINY, length = 1, org_name = to_b(Name)}; 634 | ({Name,integer}) -> #column{name = to_b(Name), type = ?TYPE_LONGLONG, length = 20, org_name = to_b(Name)} 635 | end, Columns). 636 | 637 | to_b(Atom) when is_atom(Atom) -> atom_to_binary(Atom,latin1); 638 | to_b(Bin) when is_binary(Bin) -> Bin. 639 | 640 | to_a(Atom) when is_binary(Atom) -> binary_to_atom(Atom,latin1); 641 | to_a(Atom) when is_atom(Atom) -> Atom. 642 | 643 | 644 | 645 | 646 | terminate(_Reason,_) -> 647 | ok. 648 | -------------------------------------------------------------------------------- /src/sqlapi_app.erl: -------------------------------------------------------------------------------- 1 | -module(sqlapi_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | %% =================================================================== 9 | %% Application callbacks 10 | %% =================================================================== 11 | 12 | start(_StartType, _StartArgs) -> 13 | sqlapi_sup:start_link(). 14 | 15 | stop(_State) -> 16 | ok. 17 | -------------------------------------------------------------------------------- /src/sqlapi_ram_table.erl: -------------------------------------------------------------------------------- 1 | -module(sqlapi_ram_table). 2 | -include("../include/sqlapi.hrl"). 3 | 4 | 5 | -export([tables/0, columns/1, create_table/2, drop_table/1]). 6 | -export([select/2, insert/2, update/3, delete/2]). 7 | 8 | -export([start_link/0]). 9 | -export([init/1, handle_call/3, terminate/2]). 10 | 11 | 12 | tables() -> 13 | [atom_to_binary(Name,latin1) || {Name,_} <- ets:tab2list(sql_ram_tables)]. 14 | 15 | columns(Name) -> 16 | try erlang:binary_to_existing_atom(Name,latin1) of 17 | NameAtom -> 18 | case ets:lookup(sql_ram_tables, NameAtom) of 19 | [{NameAtom,Spec}] -> Spec; 20 | [] -> {error, no_table} 21 | end 22 | catch 23 | _:_ -> {error, no_table} 24 | end. 25 | 26 | 27 | 28 | create_table(TableName, TableSpec) when is_atom(TableName) -> 29 | gen_server:call(?MODULE, {create_table, TableName, TableSpec}). 30 | 31 | drop_table(TableName) when is_atom(TableName) -> 32 | gen_server:call(?MODULE, {drop_table, TableName}). 33 | 34 | 35 | 36 | 37 | 38 | select(Table, #sql_filter{conditions = Conditions, columns = Columns, limit = Limit} = Filter) -> 39 | case columns(Table) of 40 | {error, no_table} -> 41 | {error, 1036, <<"cannot select from non-existing ",Table/binary>>}; 42 | TableColumns -> 43 | 44 | % ColumnIndex = lists:zipwith(fun({Col,_},I) -> {Col,I} end, TableColumns, lists:seq(1,length(TableColumns))), 45 | % Head = list_to_tuple([list_to_atom("$" ++ integer_to_list(proplists:get_value(Col,ColumnIndex))) || Col <- Columns]), 46 | % MatchSpec = sql_to_ms(Conditions, ColumnIndex), 47 | % Output = ['$$'], 48 | % MS = [{Head, MatchSpec, Output}], 49 | 50 | % Select = case Limit of 51 | % undefined -> ets:select(binary_to_existing_atom(Table,latin1),MS); 52 | % _ -> ets:select(binary_to_existing_atom(Table,latin1),MS,Limit) 53 | % end, 54 | % Select1 = case Select of 55 | % {error, E} -> 56 | % {error, 500, <<"Failed to select: ",(atom_to_binary(E,latin1))>>}; 57 | % '$end_of_table' -> []; 58 | % {Selection0, _Continuation0} -> Selection0; 59 | % Selection0 when is_list(Selection0) -> Selection0 60 | % end, 61 | % if 62 | % is_list(Select1) -> 63 | % NamedRows = [ maps:from_list( 64 | % lists:zipwith(fun({Col,_},V) -> {Col,V} end, TableColumns,Row) 65 | % ) || Row <- Select1], 66 | % sqlapi:apply_sql_filter(NamedRows, Filter); 67 | % true -> 68 | % Select1 69 | % end 70 | 71 | 72 | Rows = [lists:zipwith(fun({C,_},E) -> {C,E} end,TableColumns,tuple_to_list(R)) || R <- 73 | ets:tab2list(binary_to_existing_atom(Table,latin1))], 74 | Reply = sqlapi:apply_sql_filter(Rows, Filter), 75 | Reply 76 | end. 77 | 78 | 79 | % sql_to_ms(undefined, _) -> []; 80 | % sql_to_ms(Conditions, Map) -> [sql_to_ms0(Conditions,Map)]. 81 | 82 | 83 | % sql_to_ms0({condition, Op, Arg1, Arg2}, Map) -> 84 | % Op1 = case Op of 85 | % nexo_and -> 'and'; 86 | % nexo_or -> 'or'; 87 | % eq -> '=='; 88 | % lt -> '<'; 89 | % gt -> '>'; 90 | % lte -> '=<'; 91 | % gte -> '>=' 92 | % end, 93 | % {Op1, sql_to_ms0(Arg1, Map), sql_to_ms0(Arg2, Map)}; 94 | 95 | % sql_to_ms0({key, Key, _, _}, Map) when is_atom(Key) -> 96 | % proplists:get_value(Key, Map); 97 | 98 | % sql_to_ms0({key, Key, _, _}, Map) -> 99 | % proplists:get_value(erlang:binary_to_existing_atom(Key,latin1), Map); 100 | 101 | % sql_to_ms0({value, _, Arg}, _) -> Arg. 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | insert(Table, Rows) -> 112 | case columns(Table) of 113 | {error, no_table} -> 114 | {error, 1036, <<"cannot insert into non-existing ",Table/binary>>}; 115 | Columns -> 116 | Tuples = lists:map(fun(R) -> list_to_tuple([maps:get(atom_to_binary(C,latin1),R,undefined) || {C,_} <- Columns]) end, Rows), 117 | ets:insert(binary_to_existing_atom(Table,latin1), Tuples), 118 | {ok, #{status => ok, affected_rows => length(Tuples)}} 119 | end. 120 | 121 | 122 | 123 | update(Table, Values, Conditions) when is_map(Values) -> 124 | update(Table, maps:to_list(Values), Conditions); 125 | 126 | update(Table, Values, Conditions) -> 127 | case columns(Table) of 128 | {error, no_table} -> 129 | {error, 1036, <<"cannot update non-existing ",Table/binary>>}; 130 | [{Key,_}|_] = Columns -> 131 | UpdateIndex = lists:zipwith(fun({C,_},I) -> {atom_to_binary(C,latin1),I} end, 132 | Columns, lists:seq(1,length(Columns))), 133 | Update = [{element(2,lists:keyfind(K,1,UpdateIndex)),V} || {K,V} <- Values], 134 | 135 | case select(Table, #sql_filter{conditions = Conditions, table_columns = Columns}) of 136 | {error, _, _} -> 137 | {error, 1036, <<"cannot update non-existing ",Table/binary>>}; 138 | Rows -> 139 | TableAtom = binary_to_existing_atom(Table,latin1), 140 | [ets:update_element(TableAtom,maps:get(Key,R),Update) || R <- Rows], 141 | {ok, #{status => ok, affected_rows => length(Rows)}} 142 | end 143 | end. 144 | 145 | 146 | 147 | delete(Table, Conditions) -> 148 | case columns(Table) of 149 | {error, no_table} -> 150 | {error, 1036, <<"cannot delete from non-existing ",Table/binary>>}; 151 | [{Key,_}|_] = Columns -> 152 | case select(Table, #sql_filter{conditions = Conditions, table_columns = Columns}) of 153 | {error, _, _} -> 154 | {error, 1036, <<"cannot delete from non-existing ",Table/binary>>}; 155 | Rows -> 156 | TableAtom = binary_to_existing_atom(Table,latin1), 157 | [ets:delete(TableAtom,maps:get(Key,R)) || R <- Rows], 158 | {ok, #{affected_rows => length(Rows), status => ok}} 159 | end 160 | end. 161 | 162 | 163 | 164 | 165 | start_link() -> 166 | gen_server:start_link({local,?MODULE}, ?MODULE, [], []). 167 | 168 | init([]) -> 169 | ets:new(sql_ram_tables, [public,named_table]), 170 | {ok, state}. 171 | 172 | 173 | handle_call({create_table, TableName, TableSpec}, _From, State) -> 174 | case ets:lookup(sql_ram_tables, TableName) of 175 | [] -> 176 | try ets:new(TableName, [public,named_table]) of 177 | TableName -> 178 | ets:insert(sql_ram_tables, {TableName, TableSpec}), 179 | {reply, ok, State} 180 | catch 181 | _:_ -> 182 | {reply, {error, badarg}, State} 183 | end; 184 | [_] -> 185 | {reply, {error, exists}, State} 186 | end; 187 | 188 | handle_call({drop_table, TableName}, _From, State) -> 189 | case ets:lookup(sql_ram_tables, TableName) of 190 | [] -> 191 | {reply, {error, enoent}, State}; 192 | [_] -> 193 | ets:delete(sql_ram_tables, TableName), 194 | ets:delete(TableName), 195 | {reply, ok, State} 196 | end. 197 | 198 | 199 | 200 | terminate(_,_) -> ok. 201 | 202 | 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /src/sqlapi_sup.erl: -------------------------------------------------------------------------------- 1 | -module(sqlapi_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/0]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | %% Helper macro for declaring children of supervisor 12 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 13 | 14 | %% =================================================================== 15 | %% API functions 16 | %% =================================================================== 17 | 18 | start_link() -> 19 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 20 | 21 | %% =================================================================== 22 | %% Supervisor callbacks 23 | %% =================================================================== 24 | 25 | init([]) -> 26 | RamTables = ?CHILD(sqlapi_ram_table, worker), 27 | {ok, { {one_for_one, 5, 10}, [RamTables]} }. 28 | 29 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | -------------------------------------------------------------------------------- /test/sql_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(sql_SUITE). 2 | % derived from https://github.com/altenwald/myproto under EPL: 3 | % https://github.com/altenwald/myproto/commit/d89e6cad46fa966e6149aee3ad6f0d711e6182f5 4 | -author('Manuel Rubio '). 5 | -author('Max Lapshin '). 6 | 7 | 8 | -include("../src/myproto.hrl"). 9 | -include_lib("eunit/include/eunit.hrl"). 10 | -compile(export_all). 11 | 12 | 13 | all() -> 14 | [{group,parse}, 15 | {group,queries}]. 16 | 17 | 18 | groups() -> 19 | [{parse, [parallel], [ 20 | delete_simple, 21 | delete_where, 22 | insert_simple, 23 | insert_keys, 24 | insert_set, 25 | insert_multi, 26 | describe, 27 | show, 28 | show_like, 29 | transaction, 30 | set, 31 | select_all, 32 | select_strings, 33 | select_simple, 34 | select_simple_multiparams, 35 | select_simple_subquery, 36 | select_from, 37 | select_from_subquery, 38 | select_where, 39 | select_function, 40 | select_groupby, 41 | select_orderby, 42 | select_limit, 43 | select_arithmetic, 44 | select_variable, 45 | select_in, 46 | 47 | server_select_simple, 48 | server_reject_password, 49 | server_version, 50 | server_very_long_query, 51 | update_simple, 52 | update_multiparams, 53 | update_where, 54 | backslash 55 | ]}, 56 | {queries, [parallel], [ 57 | long_query_2, 58 | select_database_before_connect, 59 | login_as_sequelpro, 60 | hanami_rom_support, 61 | rails_support, 62 | no_db_selected, 63 | describe_sql, 64 | order_by, 65 | work_with_ram_table, 66 | check_version 67 | ]}]. 68 | 69 | 70 | init_per_suite(Config) -> 71 | application:ensure_started(sqlapi), 72 | sqlapi:load_config(#{port => 4406, listener_name => test_sql, handler => test_handler, trivial => true}), 73 | Config. 74 | 75 | end_per_suite(Config) -> 76 | sqlapi:load_config(#{handler => test_handler, listener_name => test_sql, trivial => true}), 77 | Config. 78 | 79 | 80 | 81 | 82 | select_database_before_connect(_) -> 83 | {ok, C} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db"), 84 | nanomysql:command(ping, <<>>, C), 85 | [#{'database()' := undefined}] = nanomysql:select("SELECT DATABASE()", C), 86 | ok. 87 | 88 | login_as_sequelpro(_) -> 89 | {ok, C} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db"), 90 | nanomysql:command(ping, <<>>, C), 91 | {ok, _} = nanomysql:execute("SHOW VARIABLES", C), 92 | [#{'@@global.max_allowed_packet' := 4194304}] = nanomysql:select("SELECT @@global.max_allowed_packet", C), 93 | {ok, _} = nanomysql:execute("USE `test_db`", C), 94 | [#{'database()' := <<"test_db">>}] = nanomysql:select("SELECT DATABASE()", C), 95 | 96 | [#{'@@tx_isolation' := <<"REPEATABLE-READ">>}] = nanomysql:select("SELECT @@tx_isolation", C), 97 | ok. 98 | 99 | 100 | 101 | 102 | no_db_selected(_) -> 103 | {ok, C} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406"), 104 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("DESCRIBE `test`",C), 105 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("SHOW TABLES",C), 106 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("SHOW INDEX FROM `test`",C), 107 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("SHOW FIELDS FROM `test`",C), 108 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("SELECT * FROM `test`",C), 109 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("DELETE FROM `test`",C), 110 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("UPDATE `test` SET a=5",C), 111 | {error, {1046, <<"No database selected">>}} = nanomysql:execute("INSERT INTO `test` VALUES (5)",C), 112 | ok. 113 | 114 | 115 | hanami_rom_support(_) -> 116 | {ok, C} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db?login=init_db"), 117 | {ok, _} = nanomysql:execute("SET SQL_AUTO_IS_NULL=0",C), 118 | {ok, _} = nanomysql:execute("DESCRIBE `test`",C), 119 | {ok, _} = nanomysql:execute("SHOW INDEX FROM `test`",C), 120 | {ok, _} = nanomysql:execute("SELECT `CONSTRAINT_NAME` AS `name`, `COLUMN_NAME` AS `column`, " 121 | "`REFERENCED_TABLE_NAME` AS `table`, `REFERENCED_COLUMN_NAME` AS `key` FROM " 122 | "`INFORMATION_SCHEMA`.`KEY_COLUMN_USAGE` WHERE ((`TABLE_NAME` = 'test') " 123 | "AND (`TABLE_SCHEMA` = DATABASE()) AND (`CONSTRAINT_NAME` != 'PRIMARY') AND " 124 | "(`REFERENCED_TABLE_NAME` IS NOT NULL))",C), 125 | {ok, _} = nanomysql:execute("SELECT NULL AS `nil` FROM `test` LIMIT 1",C), 126 | ok. 127 | 128 | 129 | rails_support(_) -> 130 | {ok, C} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db?login=init_db"), 131 | Rows1 = nanomysql:select("SELECT table_name FROM information_schema.tables WHERE table_schema = database()",C), 132 | Rows2 = lists:sort(Rows1), 133 | 134 | [#{'Tables_in_test_db' := <<"test">>}|_] = Rows2, 135 | ok. 136 | 137 | 138 | describe_sql(_) -> 139 | {ok, C} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db?login=init_db"), 140 | Rows = nanomysql:select("DESCRIBE `test`", C), 141 | #{'Field' := <<"id">>, 'Type' := <<"varchar(255)">>} = hd(Rows), 142 | ok. 143 | 144 | 145 | 146 | work_with_ram_table(_) -> 147 | {ok, Conn} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db?login=init_db"), 148 | ok = sqlapi:create_temporary_table(user_sessions, [ 149 | {user_id, integer}, {session_id, string}, {access_time, integer} 150 | ]), 151 | {ok ,_} = nanomysql:execute("insert into user_sessions (user_id,session_id,access_time) values (1,'u1',1540123456)", Conn), 152 | [#{user_id := 1, session_id := <<"u1">>, access_time := 1540123456}] = nanomysql:select("select * from user_sessions", Conn), 153 | 154 | nanomysql:execute("update user_sessions set access_time = 14123123123 where user_id=1", Conn), 155 | [#{user_id := 1, access_time := 14123123123}] = nanomysql:select("select * from user_sessions", Conn), 156 | 157 | nanomysql:execute("delete from user_sessions where user_id=1", Conn), 158 | [] = nanomysql:select("select * from user_sessions", Conn), 159 | ok. 160 | 161 | 162 | 163 | order_by(_) -> 164 | {ok, Conn} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db?login=init_db"), 165 | ok = sqlapi:create_temporary_table(ordered_streams, [ 166 | {name,string},{title,string},{extra,string},{hls_off,boolean} 167 | ]), 168 | 169 | 170 | {ok ,_} = nanomysql:execute("insert into ordered_streams (name,title,extra,hls_off) values ('ob2','ob', '', 1)", Conn), 171 | {ok ,_} = nanomysql:execute("insert into ordered_streams (name,title,extra,hls_off) values ('ob1','ob', null, 0)", Conn), 172 | {ok ,_} = nanomysql:execute("insert into ordered_streams (name,title,extra,hls_off) values ('ob3','ob', 'aa', 1)", Conn), 173 | {ok ,_} = nanomysql:execute("insert into ordered_streams (name,title,extra,hls_off) values ('ob4','ob', null, 0)", Conn), 174 | 175 | % Эти две строчки ниже я закомментировал потому что они по определению лишены смысла 176 | % Без сортировки может быть произвольный результат 177 | % Unsorted = nanomysql:select("select name,title from ordered_streams where title='ob'", Conn), 178 | % Unsorted = nanomysql:select("select name,title from ordered_streams where title='ob' order by title", Conn), 179 | [#{name := <<"ob4">>}, #{name := <<"ob3">>}, #{name := <<"ob2">>}, #{name := <<"ob1">>}] = nanomysql:select("select * from ordered_streams where title='ob' order by name desc", Conn), 180 | [#{name := <<"ob1">>}, #{name := <<"ob4">>}, #{name := <<"ob2">>}, #{name := <<"ob3">>}] = nanomysql:select("select * from ordered_streams where title='ob' order by hls_off asc, name asc", Conn), 181 | % mysql null first when asc 182 | [#{name := <<"ob1">>}, #{name := <<"ob4">>}, #{name := <<"ob2">>}, #{name := <<"ob3">>}] = nanomysql:select("select * from ordered_streams where title='ob' order by extra asc, name asc", Conn), 183 | % mysql null last when desc 184 | [#{name := <<"ob3">>}, #{name := <<"ob2">>}, #{name := <<"ob1">>}, #{name := <<"ob4">>}] = nanomysql:select("select * from ordered_streams where title='ob' order by extra desc, name asc", Conn), 185 | [] = nanomysql:select("select title,name from ordered_streams where title='ob!' order by name", Conn), 186 | try nanomysql:select("select * from ordered_streams where title='ob' order by name asc, nnaammee desc", Conn) of 187 | _ -> ct:fail("no error on unknown column") 188 | catch 189 | error:{1036, <<"unknown column in order clause">>} -> ok 190 | end. 191 | 192 | 193 | check_version(_) -> 194 | {ok, C} = nanomysql:connect("mysql://user:pass@127.0.0.1:4406/test_db"), 195 | nanomysql:command(ping, <<>>, C), 196 | [#{'VERSION()' := <<"5.6.0">>}] = nanomysql:select("SELECT VERSION()", C), 197 | ok. 198 | 199 | 200 | 201 | transaction(_) -> 202 | 'begin' = sql92:parse("begin"), 203 | 'commit' = sql92:parse("commit"), 204 | 'rollback' = sql92:parse("rollback"), 205 | ok. 206 | 207 | 208 | describe(_) -> 209 | #describe{table = #table{name = <<"streams">>}} = sql92:parse("DESCRIBE `streams`"). 210 | 211 | delete_simple(_) -> 212 | #delete{table=#table{name = <<"mitabla">>, alias = <<"mitabla">>}} = sql92:parse("delete from mitabla"). 213 | 214 | 215 | delete_where(_) -> 216 | #delete{ 217 | table=#table{name = <<"mitabla">>, alias = <<"mitabla">>}, 218 | conditions=#condition{ 219 | nexo=eq, 220 | op1=#key{name = <<"dato">>, alias = <<"dato">>}, 221 | op2=#value{value = <<"this ain't a love song">>} 222 | } 223 | } = sql92:parse("delete from mitabla where dato='this ain''t a love song'"). 224 | 225 | 226 | 227 | insert_simple(_) -> 228 | #insert{table = #table{name = <<"mitabla">>, alias = <<"mitabla">>}, values=[ 229 | [#value{value=1}, #value{value=2}, #value{value=3}] 230 | ]} = sql92:parse("insert into mitabla values (1,2,3)"). 231 | 232 | 233 | insert_keys(_) -> 234 | #insert{table = #table{name = <<"mitabla">>, 235 | alias = <<"mitabla">>}, 236 | values = [[#set{key = <<"id">>, 237 | value = #value{value = 1}}, 238 | #set{key = <<"author">>, 239 | value = #value{value = <<"bonjovi">>}}, 240 | #set{key = <<"song">>, 241 | value = #value{value = <<"these days">>}}]]} = 242 | sql92:parse("insert into mitabla(id,author,song) values(1,'bonjovi', 'these days')"). 243 | 244 | 245 | insert_set(_) -> 246 | A = sql92:parse("insert into mitabla(id,author,song) values(1,'bonjovi', 'these days')"), 247 | B = sql92:parse("insert into mitabla set id=1, author='bonjovi', song='these days'"), 248 | A = B. 249 | 250 | 251 | insert_multi(_) -> 252 | A = sql92:parse("insert into mitabla(id) values(1),(2)"), 253 | #insert{table = #table{name = <<"mitabla">>, alias = <<"mitabla">>}, values=[ 254 | [#set{key = <<"id">>, value = #value{value=1}}], 255 | [#set{key = <<"id">>, value = #value{value=2}}] 256 | ]} = A. 257 | 258 | show(_) -> 259 | #show{type=databases} = sql92:parse("SHOW databases"), 260 | #show{type=variables} = sql92:parse("SHOW variables"), 261 | #show{type=tables, full = true} = sql92:parse("SHOW FULL tables"), 262 | #show{type=tables, full = false} = sql92:parse("SHOW tables"), 263 | #show{type=fields,full=true,from= <<"streams">>} = sql92:parse("SHOW FULL FIELDS FROM `streams`"), 264 | #show{type=fields,full=false,from= <<"streams">>} = sql92:parse("SHOW FIELDS FROM `streams`"), 265 | #show{type=index,full=false,from= <<"streams">>} = sql92:parse("SHOW INDEX FROM `streams`"), 266 | #show{type=create_table,from= <<"streams">>} = sql92:parse("SHOW CREATE TABLE `streams`"), 267 | #show{type=tables,full=false,conditions= {like,<<"streams">>}} = sql92:parse("SHOW TABLES LIKE 'streams'"), 268 | #show{type=variables, conditions=#condition{ 269 | nexo = eq, 270 | op1 = #key{name = <<"Variable_name">>}, 271 | op2 = #value{value = <<"character_set_client">>} 272 | }} = sql92:parse("SHOW VARIABLES WHERE Variable_name = 'character_set_client'"), 273 | 274 | #show{type=collation, conditions=#condition{ 275 | nexo = eq, 276 | op1 = #key{name= <<"Charset">>}, 277 | op2 = #value{value = <<"utf8">>} 278 | }} = sql92:parse("show collation where Charset = 'utf8'"), 279 | ok. 280 | 281 | 282 | show_like(_) -> 283 | #show{type=variables, conditions = {like, <<"sql_mode">>}} = sql92:parse("SHOW VARIABLES LIKE 'sql_mode'"), 284 | ok. 285 | 286 | 287 | 288 | set(_) -> 289 | #system_set{query=[{#variable{name = <<"a">>, scope = session},#value{value=0}}]} = sql92:parse("SET a=0"), 290 | #system_set{query=[{#variable{name = <<"NAMES">>},#value{value= <<"utf8">>}}]} = sql92:parse("SET NAMES 'utf8'"), 291 | #system_set{query=[{#variable{name = <<"NAMES">>},#value{value= <<"utf8">>}}]} = sql92:parse("SET NAMES utf8"), 292 | 293 | #system_set{query=[ 294 | {#variable{name = <<"SQL_AUTO_IS_NULL">>, scope = session}, #value{value=0}}, 295 | {#variable{name = <<"NAMES">>},#value{value= <<"utf8">>}}, 296 | {#variable{name = <<"wait_timeout">>, scope = local}, #value{value=2147483}} 297 | ]} = sql92:parse("SET SQL_AUTO_IS_NULL=0, NAMES 'utf8', @@wait_timeout = 2147483"), 298 | 299 | 300 | #system_set{query = [ 301 | {#variable{name = <<"SESSION.sql_mode">>, scope = local}, #function{}}, 302 | {#variable{name = <<"SESSION.sql_auto_is_null">>, scope=local},#value{value= 0}}, 303 | {#variable{name = <<"SESSION.wait_timeout">>, scope = local}, #value{value=2147483}} 304 | ]} = sql92:parse("SET @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), " 305 | " @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483"), 306 | ok. 307 | 308 | 309 | select_variable(_) -> 310 | #select{params = [#variable{name = <<"max_allowed_packet">>, scope = local}]} = 311 | sql92:parse("SELECT @@max_allowed_packet"), 312 | #select{params = [#variable{name = <<"global.max_allowed_packet">>, scope = local}]} = 313 | sql92:parse("SELECT @@global.max_allowed_packet"), 314 | 315 | #select{params = [#variable{name = <<"global.max_allowed_packet">>, scope = local}], limit = 1} = 316 | sql92:parse("SELECT @@global.max_allowed_packet limit 1"), 317 | 318 | #select{tables = [#table{name= {<<"information_schema">>,<<"key_column_usage">>}}]} = 319 | sql92:parse("SELECT `CONSTRAINT_NAME` AS `name`, `COLUMN_NAME` AS `column`, `REFERENCED_TABLE_NAME` AS `table`, " 320 | "`REFERENCED_COLUMN_NAME` AS `key` FROM `INFORMATION_SCHEMA`.`KEY_COLUMN_USAGE` WHERE " 321 | "((`TABLE_NAME` = 'streams') AND (`TABLE_SCHEMA` = DATABASE()) AND (`CONSTRAINT_NAME` != 'PRIMARY') " 322 | "AND (`REFERENCED_TABLE_NAME` IS NOT NULL))"), 323 | 324 | ok. 325 | 326 | 327 | select_in(_) -> 328 | #select{params=[#all{}], 329 | conditions=#condition{nexo=in, 330 | op1 = #key{name= <<"n">>}, 331 | op2 = #subquery{subquery = [<<"a">>,<<"b">>]} 332 | } 333 | } = sql92:parse(<<"SELECT * from b where n in ('a','b') order by a">>), 334 | #select{params=[#all{}], 335 | conditions=#condition{nexo=not_in, 336 | op1 = #key{name= <<"n">>}, 337 | op2 = #subquery{subquery = [<<"a">>,<<"b">>]} 338 | } 339 | } = sql92:parse(<<"SELECT * from b where n not in ('a','b') order by a">>), 340 | ok. 341 | 342 | 343 | select_all(_) -> 344 | #select{params=[#all{}]} = sql92:parse("select *"), 345 | #select{params=[#all{}]} = sql92:parse("SELECT *"), 346 | #select{params=[#all{}]} = sql92:parse(" Select * "), 347 | ok. 348 | 349 | select_strings(_) -> 350 | #select{params = [#value{value = <<"hola'mundo">>}]} = sql92:parse("select 'hola''mundo'"). 351 | 352 | 353 | select_simple(_) -> 354 | ?assertEqual(sql92:parse("select 'hi' as message"), 355 | #select{params=[#value{name = <<"message">>,value = <<"hi">>}]} 356 | ), 357 | ?assertEqual(sql92:parse("select 'hi'"), 358 | #select{params=[#value{value = <<"hi">>}]} 359 | ), 360 | ?assertEqual(sql92:parse("select hi"), 361 | #select{params=[#key{alias = <<"hi">>,name = <<"hi">>}]} 362 | ), 363 | ?assertEqual(sql92:parse("select hi as hello"), 364 | #select{params=[#key{alias = <<"hello">>,name = <<"hi">>}]} 365 | ), 366 | ?assertEqual(sql92:parse("select a.hi"), 367 | #select{params=[#key{alias = <<"hi">>,name = <<"hi">>,table = <<"a">>}]} 368 | ), 369 | ?assertEqual(sql92:parse("select aa.hi as hello"), 370 | #select{params=[#key{alias = <<"hello">>,name = <<"hi">>,table = <<"aa">>}]} 371 | ), 372 | ok. 373 | 374 | select_simple_multiparams(_) -> 375 | ?assertEqual(sql92:parse("select 'hi' as message, 1 as id"), 376 | #select{params=[#value{name = <<"message">>,value = <<"hi">>},#value{name = <<"id">>,value=1}]} 377 | ), 378 | ?assertEqual(sql92:parse("select 'hi', 1"), 379 | #select{params=[#value{value = <<"hi">>},#value{value=1}]} 380 | ), 381 | ?assertEqual(sql92:parse("select hi, message"), 382 | #select{params=[#key{alias = <<"hi">>,name = <<"hi">>}, 383 | #key{alias = <<"message">>,name = <<"message">>}]} 384 | ), 385 | ?assertEqual(sql92:parse("select hi as hello, message as msg"), 386 | #select{params=[#key{alias = <<"hello">>,name = <<"hi">>}, 387 | #key{alias = <<"msg">>,name = <<"message">>}]} 388 | ), 389 | ?assertEqual(sql92:parse("select a.hi, a.message"), 390 | #select{params=[#key{alias = <<"hi">>,name = <<"hi">>,table = <<"a">>}, 391 | #key{alias = <<"message">>,name = <<"message">>,table = <<"a">>}]} 392 | ), 393 | ?assertEqual(sql92:parse("select aa.hi as hello, aa.message as msg"), 394 | #select{params=[#key{alias = <<"hello">>,name = <<"hi">>,table = <<"aa">>}, 395 | #key{alias = <<"msg">>,name = <<"message">>,table = <<"aa">>}]} 396 | ), 397 | ?assertEqual(sql92:parse("select a.*, b.*"), 398 | #select{params=[#all{table = <<"a">>}, #all{table = <<"b">>}]} 399 | ), 400 | ?assertEqual(sql92:parse("select *, a.*, b.*"), 401 | #select{params=[#all{}, #all{table = <<"a">>}, #all{table = <<"b">>}]} 402 | ), 403 | ok. 404 | 405 | select_simple_subquery(_) -> 406 | ?assertEqual(sql92:parse("select (select *)"), 407 | #select{params=[#subquery{subquery=#select{params=[#all{}]}}]} 408 | ), 409 | ?assertEqual(sql92:parse("select (select uno) as uno, dos"), 410 | #select{params=[#subquery{name = <<"uno">>, 411 | subquery=#select{params=[#key{alias = <<"uno">>,name = <<"uno">>}]}}, 412 | #key{alias = <<"dos">>,name = <<"dos">>}]} 413 | ), 414 | ok. 415 | 416 | select_from(_) -> 417 | ?assertEqual(sql92:parse("select * from data"), 418 | #select{params=[#all{}],tables=[#table{name = <<"data">>,alias = <<"data">>}]} 419 | ), 420 | ?assertEqual(sql92:parse("select uno, dos from data, data2"), 421 | #select{params = [#key{alias = <<"uno">>,name = <<"uno">>}, 422 | #key{alias = <<"dos">>,name = <<"dos">>}], 423 | tables = [#table{name = <<"data">>,alias = <<"data">>}, 424 | #table{name = <<"data2">>,alias = <<"data2">>}]} 425 | ), 426 | ?assertEqual(sql92:parse("select d.uno, d2.dos from data as d, data2 as d2"), 427 | #select{params = [#key{alias = <<"uno">>,name = <<"uno">>, 428 | table = <<"d">>}, 429 | #key{alias = <<"dos">>,name = <<"dos">>,table = <<"d2">>}], 430 | tables = [#table{name = <<"data">>,alias = <<"d">>}, 431 | #table{name = <<"data2">>,alias = <<"d2">>}]} 432 | ), 433 | 434 | #select{params = [#all{}], tables =[#table{name= <<"streams">>}], 435 | order = [#order{key= <<"name">>, sort = asc}], limit =1} = 436 | sql92:parse("SELECT `streams`.* FROM `streams` ORDER BY `streams`.`name` ASC LIMIT 1"), 437 | 438 | #select{params = [#key{name = <<"table_name">>}], 439 | tables = [#table{name = {<<"information_schema">>,<<"tables">>}}]} = 440 | sql92:parse("SELECT table_name FROM information_schema.tables WHERE table_schema = database()"), 441 | ok. 442 | 443 | select_from_subquery(_) -> 444 | ?assertEqual(sql92:parse("select * from (select 1 as uno,2 as dos)"), 445 | #select{ 446 | params = [#all{}], 447 | tables = 448 | [#subquery{ 449 | subquery = 450 | #select{ 451 | params = 452 | [#value{name = <<"uno">>,value = 1}, 453 | #value{name = <<"dos">>,value = 2}]}}]} 454 | ), 455 | ?assertEqual(sql92:parse("select (select 1) as id, t.uno from (select 2) as t"), 456 | #select{ 457 | params = 458 | [#subquery{ 459 | name = <<"id">>, 460 | subquery = 461 | #select{ 462 | params = [#value{name = undefined,value = 1}]}}, 463 | #key{alias = <<"uno">>,name = <<"uno">>,table = <<"t">>}], 464 | tables = 465 | [#subquery{ 466 | name = <<"t">>, 467 | subquery = 468 | #select{ 469 | params = [#value{value = 2}]}}]} 470 | ), 471 | ?assertEqual(sql92:parse("select * from clientes where id in ( 1, 2, 3 )"), 472 | #select{params = [#all{}], 473 | tables = [#table{name = <<"clientes">>, 474 | alias = <<"clientes">>}], 475 | conditions = #condition{nexo = in, 476 | op1 = #key{alias = <<"id">>,name = <<"id">>}, 477 | op2 = #subquery{subquery = [1,2,3]}}} 478 | ), 479 | ok. 480 | 481 | select_where(_) -> 482 | ?assertEqual(sql92:parse("select * from tabla where uno=1"), 483 | #select{params = [#all{}], 484 | tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], 485 | conditions = #condition{nexo = eq, 486 | op1 = #key{alias = <<"uno">>,name = <<"uno">>}, 487 | op2 = #value{value = 1}}} 488 | ), 489 | ?assertEqual(sql92:parse("select * from tabla where uno=1 and dos<2"), 490 | #select{ 491 | params = [#all{}], 492 | tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], 493 | conditions = 494 | #condition{ 495 | nexo = nexo_and, 496 | op1 = 497 | #condition{ 498 | nexo = eq, 499 | op1 = 500 | #key{alias = <<"uno">>,name = <<"uno">>}, 501 | op2 = #value{value = 1}}, 502 | op2 = 503 | #condition{ 504 | nexo = lt, 505 | op1 = 506 | #key{alias = <<"dos">>,name = <<"dos">>}, 507 | op2 = #value{value = 2}}}} 508 | ), 509 | ?assertEqual(sql92:parse("select * from tabla where uno=1 and dos<2 and tres>3"), 510 | #select{ 511 | params = [#all{}], 512 | tables = [#table{name = <<"tabla">>,alias = <<"tabla">>}], 513 | conditions = 514 | #condition{ 515 | nexo = nexo_and, 516 | op1 = 517 | #condition{ 518 | nexo = eq, 519 | op1 = 520 | #key{alias = <<"uno">>,name = <<"uno">>}, 521 | op2 = #value{value = 1}}, 522 | op2 = 523 | #condition{ 524 | nexo = nexo_and, 525 | op1 = 526 | #condition{ 527 | nexo = lt, 528 | op1 = 529 | #key{alias = <<"dos">>,name = <<"dos">>}, 530 | op2 = #value{value = 2}}, 531 | op2 = 532 | #condition{ 533 | nexo = gt, 534 | op1 = 535 | #key{alias = <<"tres">>,name = <<"tres">>}, 536 | op2 = #value{value = 3}}}}} 537 | ), 538 | ?assertEqual( 539 | sql92:parse("select * from tabla where uno=1 and dos<=2 and tres>=3"), 540 | sql92:parse("select * from tabla where uno=1 and (dos=<2 and tres=>3)") 541 | ), 542 | ?assertEqual( 543 | sql92:parse("select * from tabla where tres<>3"), 544 | sql92:parse("select * from tabla where tres!=3") 545 | ), 546 | ?assertEqual( 547 | sql92:parse("select * from a where (a=1 and b=2) and c=3"), 548 | #select{ 549 | params = [#all{}], 550 | tables = [#table{name = <<"a">>,alias = <<"a">>}], 551 | conditions = 552 | #condition{ 553 | nexo = nexo_and, 554 | op1 = 555 | #condition{ 556 | nexo = nexo_and, 557 | op1 = 558 | #condition{ 559 | nexo = eq, 560 | op1 = #key{alias = <<"a">>,name = <<"a">>}, 561 | op2 = #value{value = 1}}, 562 | op2 = 563 | #condition{ 564 | nexo = eq, 565 | op1 = #key{alias = <<"b">>,name = <<"b">>}, 566 | op2 = #value{value = 2}}}, 567 | op2 = 568 | #condition{ 569 | nexo = eq, 570 | op1 = #key{alias = <<"c">>,name = <<"c">>}, 571 | op2 = #value{value = 3}}}} 572 | ), 573 | ok. 574 | 575 | select_function(_) -> 576 | #select{params = [#function{name = <<"cast">>, params=[#value{value = <<"test plain returns">>}, {<<"CHAR">>, 60}]}]} 577 | = sql92:parse("SELECT CAST('test plain returns' AS CHAR(60)) AS anon_1"), 578 | 579 | #select{params = [#function{name = <<"cast">>, params=[#value{value = <<"test collated returns">>}, <<"CHAR">>]}]} 580 | = sql92:parse("SELECT CAST('test collated returns' AS CHAR CHARACTER SET utf8) COLLATE utf8_bin AS anon_1"), 581 | 582 | #select{params = [#function{name = <<"database">>, params=[]}]} 583 | = sql92:parse("select database()"), 584 | ?assertEqual(sql92:parse("select count(*)"), 585 | #select{params = [#function{name = <<"count">>, params = [#all{}]}]} 586 | ), 587 | ?assertEqual(sql92:parse("select concat('hola', 'mundo')"), 588 | #select{params = [#function{name = <<"concat">>, 589 | params = [#value{value = <<"hola">>}, 590 | #value{value = <<"mundo">>}]}]} 591 | ), 592 | ok. 593 | 594 | select_groupby(_) -> 595 | ?assertEqual(sql92:parse("select fecha, count(*) as total from datos group by fecha"), 596 | #select{params = [#key{alias = <<"fecha">>, 597 | name = <<"fecha">>}, 598 | #function{name = <<"count">>, 599 | params = [#all{}], 600 | alias = <<"total">>}], 601 | tables = [#table{name = <<"datos">>,alias = <<"datos">>}], 602 | group = [<<"fecha">>]} 603 | ), 604 | ?assertEqual(sql92:parse("select fecha, count(*) from datos group by fecha"), 605 | #select{params = [#key{alias = <<"fecha">>, 606 | name = <<"fecha">>}, 607 | #function{name = <<"count">>, 608 | params = [#all{}], 609 | alias = undefined}], 610 | tables = [#table{name = <<"datos">>,alias = <<"datos">>}], 611 | group = [<<"fecha">>]} 612 | ), 613 | ?assertEqual(sql92:parse("select * from a group by 1"), 614 | #select{params = [#all{}], 615 | tables = [#table{name = <<"a">>,alias = <<"a">>}], 616 | group = [1]} 617 | ), 618 | ok. 619 | 620 | select_orderby(_) -> 621 | ?assertEqual(sql92:parse("select * from tabla order by 1"), 622 | #select{ 623 | params=[#all{}], 624 | tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], 625 | order=[#order{key=1,sort=asc}] 626 | } 627 | ), 628 | ?assertEqual(sql92:parse("select * from tabla order by 1 desc"), 629 | #select{ 630 | params=[#all{}], 631 | tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], 632 | order=[#order{key=1,sort=desc}] 633 | } 634 | ), 635 | ok. 636 | 637 | select_limit(_) -> 638 | ?assertEqual(sql92:parse("select * from tabla limit 10"), 639 | #select{ 640 | params=[#all{}], 641 | tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], 642 | limit=10 643 | } 644 | ), 645 | ?assertEqual(sql92:parse("select * from tabla limit 10 offset 5"), 646 | #select{ 647 | params=[#all{}], 648 | tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], 649 | limit=10, 650 | offset=5 651 | } 652 | ), 653 | % see https://dev.mysql.com/doc/refman/5.7/en/select.html 654 | ?assertEqual(sql92:parse("select * from tabla limit 5, 10"), 655 | #select{ 656 | params=[#all{}], 657 | tables=[#table{alias = <<"tabla">>, name = <<"tabla">>}], 658 | limit=10, 659 | offset=5 660 | } 661 | ), 662 | ok. 663 | 664 | select_arithmetic(_) -> 665 | ?assertEqual( 666 | #select{params = [#operation{type = <<"+">>, 667 | op1 = #value{value = 2}, 668 | op2 = #value{value = 3}}]}, 669 | sql92:parse("select 2+3") 670 | ), 671 | ?assertEqual( 672 | sql92:parse("select 2+3"), 673 | sql92:parse("select (2+3)") 674 | ), 675 | ?assertNotEqual( 676 | sql92:parse("select (2+3)*4"), 677 | sql92:parse("select 2+3*4") 678 | ), 679 | ?assertEqual( 680 | #select{params = [#operation{type = <<"*">>, 681 | op1 = #operation{type = <<"+">>, 682 | op1 = #value{value = 2}, 683 | op2 = #value{value = 3}}, 684 | op2 = #value{value = 4}}]}, 685 | sql92:parse("select (2+3)*4") 686 | ), 687 | ?assertEqual( 688 | #select{params = [#all{}], 689 | tables = [#table{name = <<"data">>,alias = <<"data">>}], 690 | conditions = #condition{nexo = eq, 691 | op1 = #key{alias = <<"a">>,name = <<"a">>}, 692 | op2 = #operation{type = <<"*">>, 693 | op1 = #key{alias = <<"b">>,name = <<"b">>}, 694 | op2 = #value{value = 3}}}}, 695 | sql92:parse("select * from data where a = b*3") 696 | ), 697 | ok. 698 | 699 | 700 | 701 | 702 | 703 | 704 | update_simple(_) -> 705 | #update{ 706 | table=#table{name = <<"streams">>}, 707 | set=[ 708 | #set{key = <<"title">>, value=#value{value = <<"aaa">>}}, 709 | #set{key = <<"comment">>, value=#value{value = <<>>}}], 710 | conditions=#condition{ 711 | nexo=nexo_and, 712 | op1=#condition{nexo=eq, op1=#key{name = <<"name">>, table = <<"streams">>}, op2=#value{value = <<"updat2">>}}, 713 | op2=#condition{nexo=eq, op1=#key{name = <<"server">>, table = <<"streams">>}, op2=#value{value = <<"localhost">>}} 714 | } 715 | } = sql92:parse("UPDATE `streams` SET `streams`.`title`='aaa', `streams`.`comment`='' WHERE `streams`.`name`='updat2' AND `streams`.`server`='localhost'"), 716 | ?assertEqual( 717 | sql92:parse("update mitabla set dato=1"), 718 | #update{ 719 | table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, 720 | set=[#set{key = <<"dato">>, value=#value{value=1}}] 721 | } 722 | ), 723 | ?assertEqual( 724 | sql92:parse(" Update mitabla SET dato = 1 "), 725 | sql92:parse("UPDATE mitabla SET dato=1") 726 | ), 727 | ok. 728 | 729 | update_multiparams(_) -> 730 | ?assertEqual( 731 | sql92:parse("update mitabla set dato1=1, dato2='bon jovi', dato3='this ain''t a love song'"), 732 | #update{ 733 | table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, 734 | set=[ 735 | #set{key = <<"dato1">>, value=#value{value = 1}}, 736 | #set{key = <<"dato2">>, value=#value{value = <<"bon jovi">>}}, 737 | #set{key = <<"dato3">>, value=#value{value = <<"this ain't a love song">>}} 738 | ] 739 | } 740 | ), 741 | ok. 742 | 743 | update_where(_) -> 744 | ?assertEqual( 745 | sql92:parse("update mitabla set dato=1 where dato=5"), 746 | #update{ 747 | table=#table{alias = <<"mitabla">>, name = <<"mitabla">>}, 748 | set=[#set{key = <<"dato">>, value=#value{value=1}}], 749 | conditions=#condition{ 750 | nexo=eq, 751 | op1=#key{alias = <<"dato">>, name = <<"dato">>}, 752 | op2=#value{value=5} 753 | } 754 | } 755 | ), 756 | ok. 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | server_select_simple(_) -> 768 | {ok, LSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), 769 | {ok, ListenPort} = inet:port(LSocket), 770 | Client = spawn_link(fun() -> 771 | {ok, Sock} = nanomysql:connect("mysql://user:pass@127.0.0.1:"++integer_to_list(ListenPort)++"/dbname"), 772 | Query1 = "SELECT input,output FROM minute_stats WHERE source='net' AND time >= '2013-09-05' AND time < '2013-09-06'", 773 | {ok, {Columns1, Rows1}} = nanomysql:execute(Query1, Sock), 774 | [{<<"input">>,_}, {<<"output">>,_}] = Columns1, 775 | [ 776 | [<<"20">>,20], 777 | [<<"30">>,30], 778 | [<<"40">>,undefined] 779 | ] = Rows1, 780 | ok 781 | end), 782 | erlang:monitor(process, Client), 783 | {ok, Sock} = gen_tcp:accept(LSocket), 784 | My0 = my_protocol:init([{socket,Sock}]), 785 | {ok, My1} = my_protocol:hello(42, My0), 786 | {ok, #request{info = #user{}}, My2} = my_protocol:next_packet(My1), 787 | {ok, My3} = my_protocol:ok(My2), 788 | 789 | {ok, #request{info = #select{} = Select}, My4} = my_protocol:next_packet(My3), 790 | #select{ 791 | params = [#key{name = <<"input">>},#key{name = <<"output">>}], 792 | tables = [#table{name = <<"minute_stats">>}], 793 | conditions = #condition{nexo = nexo_and, 794 | op1 = #condition{nexo = eq, op1 = #key{name = <<"source">>},op2 = #value{value = <<"net">>}}, 795 | op2 = #condition{nexo = nexo_and, 796 | op1 = #condition{nexo = gte, op1 = #key{name = <<"time">>}, op2 = #value{value = <<"2013-09-05">>}}, 797 | op2 = #condition{nexo = lt, op1 = #key{name = <<"time">>}, op2 = #value{value = <<"2013-09-06">>}} 798 | } 799 | } 800 | } = Select, 801 | 802 | ResponseFields = { 803 | [ 804 | #column{name = <<"input">>, type=?TYPE_VARCHAR, length=20}, 805 | #column{name = <<"output">>, type=?TYPE_LONG, length = 8} 806 | ], 807 | [ 808 | [<<"20">>, 20], 809 | [<<"30">>, 30], 810 | [<<"40">>, undefined] 811 | ] 812 | }, 813 | Response = #response{status=?STATUS_OK, info = ResponseFields}, 814 | {ok, _My5} = my_protocol:send_or_reply(Response, My4), 815 | receive {'DOWN', _, _, Client, Reason} -> normal = Reason end, 816 | ok. 817 | 818 | 819 | 820 | 821 | server_reject_password(_) -> 822 | {ok, LSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), 823 | {ok, ListenPort} = inet:port(LSocket), 824 | Client = spawn_link(fun() -> 825 | case nanomysql:connect("mysql://user:pass@127.0.0.1:"++integer_to_list(ListenPort)++"/dbname") of 826 | {error,{1045,<<"password rejected">>}} -> ok; 827 | {error, E} -> error(E); 828 | {ok,_} -> error(need_to_reject) 829 | end 830 | end), 831 | erlang:monitor(process, Client), 832 | {ok, Sock} = gen_tcp:accept(LSocket), 833 | My0 = my_protocol:init([{socket,Sock}]), 834 | {ok, My1} = my_protocol:hello(42, My0), 835 | {ok, #request{info = #user{}}, My2} = my_protocol:next_packet(My1), 836 | {ok, _My3} = my_protocol:error(<<"password rejected">>, My2), 837 | 838 | receive {'DOWN', _, _, Client, Reason} -> normal = Reason end, 839 | ok. 840 | 841 | 842 | 843 | server_version(_) -> 844 | {ok, LSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), 845 | {ok, ListenPort} = inet:port(LSocket), 846 | Client = spawn_link(fun() -> 847 | {ok,C} = nanomysql:connect("mysql://user:pass@127.0.0.1:"++integer_to_list(ListenPort)++"/dbname"), 848 | Version = nanomysql:version(C), 849 | <<"5.5.6-myproto">> = Version 850 | end), 851 | erlang:monitor(process, Client), 852 | {ok, Sock} = gen_tcp:accept(LSocket), 853 | My0 = my_protocol:init([{socket,Sock}]), 854 | {ok, My1} = my_protocol:hello(42, My0), 855 | {ok, #request{info = #user{}}, My2} = my_protocol:next_packet(My1), 856 | {ok, _My3} = my_protocol:ok(My2), 857 | 858 | receive {'DOWN', _, _, Client, Reason} -> normal = Reason end, 859 | ok. 860 | 861 | 862 | 863 | server_very_long_query(_) -> 864 | Value = binary:copy(<<"0123456789">>, 2177721), 865 | Query = iolist_to_binary(["INSERT INTO photos (data) VALUES ('", Value, "')"]), 866 | 867 | {ok, LSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), 868 | {ok, ListenPort} = inet:port(LSocket), 869 | Client = spawn_link(fun() -> 870 | {ok, Sock} = nanomysql:connect("mysql://user:pass@127.0.0.1:"++integer_to_list(ListenPort)++"/dbname"), 871 | nanomysql:execute(Query, Sock), 872 | ok 873 | end), 874 | erlang:monitor(process, Client), 875 | {ok, Sock} = gen_tcp:accept(LSocket), 876 | My0 = my_protocol:init([{socket,Sock},{parse_query,false}]), 877 | {ok, My1} = my_protocol:hello(42, My0), 878 | {ok, #request{info = #user{}}, My2} = my_protocol:next_packet(My1), 879 | {ok, My3} = my_protocol:ok(My2), 880 | 881 | {ok, #request{info = Query}, My4} = my_protocol:next_packet(My3), 882 | 883 | ResponseFields = { 884 | [#column{name = <<"id">>, type=?TYPE_LONG, length = 8}], 885 | [[20]] 886 | }, 887 | Response = #response{status=?STATUS_OK, info = ResponseFields}, 888 | {ok, _My5} = my_protocol:send_or_reply(Response, My4), 889 | 890 | % receive {'DOWN', _, _, Client, Reason} -> normal = Reason end, 891 | ok. 892 | 893 | long_query_2(_) -> 894 | Values0 = binary:copy(<<"'test_name',">>, 500), 895 | Values = <>, 896 | 897 | {ok, C} = nanomysql:connect("mysql://user:user@127.0.0.1:4406/test_db?login=init_db"), 898 | nanomysql:execute(<<"select * from test where name in (", Values/binary, ")">>, C), 899 | ok. 900 | 901 | 902 | backslash(_) -> 903 | %% translate \x specials 904 | #select{params = [#value{ 905 | value= <<0,$',$",8,10,13,9,26,$\\>>}]} = sql92:parse("select '\\0\\'\\\"\\b\\n\\r\\t\\Z\\'"), 906 | %% single \ 907 | #select{params = [#value{ 908 | value= <<$\\>>}]} = sql92:parse("select '\\'"), 909 | %% keep \ for other 910 | #select{params = [#value{ 911 | value= <<"a \\a\\ ">>}]} = sql92:parse("select '\a\ \\a\\ '"). 912 | -------------------------------------------------------------------------------- /test/sql_ets_api_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(sql_ets_api_SUITE). 2 | 3 | -compile(export_all). 4 | 5 | all() -> 6 | [{group, ets}]. 7 | 8 | 9 | groups() -> 10 | [{ets, [], [ 11 | full_cycle 12 | ]}]. 13 | 14 | 15 | 16 | init_per_suite(Config) -> 17 | application:ensure_started(sqlapi), 18 | sqlapi:load_config(#{port => 4406, listener_name => test_sql, handler => sql_ets_api, trivial => true}), 19 | Config. 20 | 21 | end_per_suite(Config) -> 22 | sqlapi:load_config(#{handler => sql_ets_api, listener_name => test_sql, trivial => true}), 23 | Config. 24 | 25 | 26 | full_cycle(_) -> 27 | {ok, Conn} = nanomysql:connect("mysql://login:pass@127.0.0.1:4406/ets?login=init_db"), 28 | {ok, _} = nanomysql:execute("SELECT create_ram_table('sessions','session_id:string','user_id:integer','last_seen_at:integer')", Conn), 29 | {ok ,_} = nanomysql:execute("insert into sessions (user_id,session_id,last_seen_at) values (1,'u1',1540123456)", Conn), 30 | [#{user_id := 1, session_id := <<"u1">>, last_seen_at := 1540123456}] = nanomysql:select("select * from sessions", Conn), 31 | 32 | {ok, _} = nanomysql:execute("update sessions set last_seen_at = 14123123123 where user_id=1", Conn), 33 | [#{user_id := 1, last_seen_at := 14123123123}] = nanomysql:select("select * from sessions", Conn), 34 | 35 | nanomysql:execute("delete from sessions where user_id=1", Conn), 36 | [] = nanomysql:select("select * from sessions", Conn), 37 | {ok, _} = nanomysql:execute("SELECT drop_ram_table('sessions')", Conn), 38 | {error, {1146,_}} = nanomysql:execute("select * from sessions", Conn), 39 | ok. 40 | -------------------------------------------------------------------------------- /test/test_handler.erl: -------------------------------------------------------------------------------- 1 | -module (test_handler). 2 | -include("../src/myproto.hrl"). 3 | 4 | 5 | 6 | -export([authorize/4,connect_db/2, database/1, databases/1, tables/1, columns/2, version/1, terminate/2]). 7 | -export([select/3, insert/3, update/4, delete/3, fncall/3]). 8 | 9 | 10 | 11 | 12 | -define(ERR_WRONG_PASS, {error, <<"Password incorrect!">>}). 13 | -define(ERR_WRONG_USER, {error, <<"No such user!">>}). 14 | -define(ERR_LOGIN_DISABLED, {error, <<"Login disabled">>}). 15 | -define(ERR_INFO(Code, Desc), #response{status=?STATUS_ERR, error_code=Code, info=Desc}). 16 | 17 | 18 | -record(my, { 19 | db 20 | }). 21 | 22 | authorize(<<"user">>, _Pass, _Hash, _) -> {ok, #my{}}; 23 | authorize(_,_,_,_) -> {error, <<"Bad user">>}. 24 | 25 | 26 | connect_db(DB, My) -> {ok, My#my{db=DB}}. 27 | database(#my{db=DB}) -> DB. 28 | version(_) -> <<"5.6.0">>. 29 | databases(_) -> [<<"test_db">>]. 30 | tables(_) -> [<<"test">>]. 31 | columns(<<"test">>,_) -> [{id,string},{name,string},{url,string}]; 32 | columns(Table, _) -> sqlapi_ram_table:columns(Table). 33 | 34 | 35 | 36 | fncall(_,_,_) -> error(not_implemented). 37 | insert(Table,Values,_) -> sqlapi_ram_table:insert(Table, Values). 38 | delete(Table,Conditions,_) -> sqlapi_ram_table:delete(Table, Conditions). 39 | update(Table,Values,Conditions,_) -> sqlapi_ram_table:update(Table, Values, Conditions). 40 | 41 | 42 | select(<<"test">>, Filter, #my{}) -> 43 | Rows = [ [{id, <<"1">>}, {name, <<"stream1">>}, {url, <<"rtsp://...">>}] ], 44 | sqlapi:apply_sql_filter(Rows, Filter); 45 | select(Table,Conditions,_) -> sqlapi_ram_table:select(Table, Conditions). 46 | 47 | 48 | 49 | 50 | terminate(_Reason,_) -> 51 | ok. 52 | --------------------------------------------------------------------------------