From d21395b11ac59169cefcd3ff4a1fc4e43eb0873c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Tue, 7 Nov 2017 14:55:04 +0100 Subject: [PATCH 01/11] tvp param descriptor --- lib/tds/types.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/tds/types.ex b/lib/tds/types.ex index ec8f234..055dbb1 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -67,6 +67,7 @@ defmodule Tds.Types do @tds_data_type_nchar 0xEF @tds_data_type_xml 0xF1 @tds_data_type_udt 0xF0 + @tds_data_type_tvp 0xF3 @tds_data_type_text 0x23 @tds_data_type_image 0x22 @tds_data_type_ntext 0x63 @@ -764,6 +765,7 @@ defmodule Tds.Types do :date -> "date" :time -> "time" :smalldatetime -> "smalldatetime" + :tvp -> "#{value.vame} readonly" :binary -> encode_binary_descriptor(value) :string -> cond do From e053de6acefcce7893756479f9473fd9cdab82fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Tue, 7 Nov 2017 15:30:19 +0100 Subject: [PATCH 02/11] encode tvp type param --- lib/tds/types.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/tds/types.ex b/lib/tds/types.ex index 055dbb1..893917b 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -502,6 +502,7 @@ defmodule Tds.Types do :date -> encode_date_type(param) :time -> encode_time_type(param) :uuid -> encode_uuid_type(param) + :tvp -> encode_tvp_type(param) _ -> encode_string_type(param) end end @@ -575,6 +576,11 @@ defmodule Tds.Types do encode_data_type(%{param | type: :datetimeoffset}) end + def encode_tvp_type(%Parameter{value: value} = param) do + type = @tds_data_type_tvp + data = <> <> <<0, 0>> <> <<0, 0>> <> <<0, 0>> + {type, data, []} + end def encode_binary_type(%Parameter{value: value} = param) when value == "" do From d765ee55275bd49458c15994fb772a52be63a305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Wed, 8 Nov 2017 10:25:30 +0100 Subject: [PATCH 03/11] encode tvp data --- lib/tds/types.ex | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/tds/types.ex b/lib/tds/types.ex index 893917b..9f50f48 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -576,7 +576,7 @@ defmodule Tds.Types do encode_data_type(%{param | type: :datetimeoffset}) end - def encode_tvp_type(%Parameter{value: value} = param) do + def encode_tvp_type(%Parameter{}) do type = @tds_data_type_tvp data = <> <> <<0, 0>> <> <<0, 0>> <> <<0, 0>> {type, data, []} @@ -975,6 +975,33 @@ defmodule Tds.Types do def encode_data(@tds_data_type_bigvarbinary, value, _), do: <> <> value + @doc """ + Data Encoding TVP type + """ + def encode_data(@tds_data_type_tvp, value, _attrs) when is_nil(value), + do: <<0xFF :: little-unsigned-16, 0x00, 0x00 >> + + def encode_data(@tds_data_type_tvp, %{columns: columns, rows: rows}, _attrs) do + column_length = <> + {column_attrs, column_meta} = Enum.reduce(columns, {[], <<>>}, fn (%Parameter{} = param, {attrs, acc_bin}) -> + {bin_type, data, attr} = encode_data_type(param) + bin = acc_bin <> <<0x00 :: little-unsigned-32, 0x00 :: little-unsigned-16 >> <> data <> <<0, 0>> + + {[{bin_type, attr} | attrs], bin} + end) + + row_data = Enum.reduce(rows, <<>>, fn params -> + column_attrs + |> Enum.zip(params) + |> Enum.reduce(<<>>, fn ({{type, attr}, param}, acc) -> + acc <> encode_data(type, param, attr) + end) + << 0x01 >> <> column_attrs + end) + + column_length <> column_meta <> <<0x00>> <> row_data <> <<0x00>> + end + @doc """ Data Encoding String Types """ From cb9ac48406f34596576316077adeacdbba05ca0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Fri, 10 Nov 2017 10:40:25 +0100 Subject: [PATCH 04/11] correctly encode tvp type --- lib/tds/types.ex | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/tds/types.ex b/lib/tds/types.ex index 9f50f48..dd2cf07 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -578,7 +578,7 @@ defmodule Tds.Types do def encode_tvp_type(%Parameter{}) do type = @tds_data_type_tvp - data = <> <> <<0, 0>> <> <<0, 0>> <> <<0, 0>> + data = <> <> <<0, 0, 0>> {type, data, []} end @@ -978,7 +978,7 @@ defmodule Tds.Types do @doc """ Data Encoding TVP type """ - def encode_data(@tds_data_type_tvp, value, _attrs) when is_nil(value), + def encode_data(@tds_data_type_tvp, %{columns: nil}, _attrs), do: <<0xFF :: little-unsigned-16, 0x00, 0x00 >> def encode_data(@tds_data_type_tvp, %{columns: columns, rows: rows}, _attrs) do @@ -990,16 +990,16 @@ defmodule Tds.Types do {[{bin_type, attr} | attrs], bin} end) - row_data = Enum.reduce(rows, <<>>, fn params -> - column_attrs + row_data = Enum.reduce(rows, <<>>, fn (params, row_acc) -> + row_bin = column_attrs |> Enum.zip(params) |> Enum.reduce(<<>>, fn ({{type, attr}, param}, acc) -> - acc <> encode_data(type, param, attr) + foo = acc <> encode_data(type, param, attr) end) - << 0x01 >> <> column_attrs + row_acc <> << 0x01 >> <> row_bin end) - column_length <> column_meta <> <<0x00>> <> row_data <> <<0x00>> + column_length <> column_meta <> row_data <> <<0x00>> end @doc """ From 6a93a1ff458232430b0e9c888c2c5a05ffdde538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Fri, 10 Nov 2017 10:41:52 +0100 Subject: [PATCH 05/11] tvp test --- test/tvp_test.exs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/tvp_test.exs diff --git a/test/tvp_test.exs b/test/tvp_test.exs new file mode 100644 index 0000000..d97843b --- /dev/null +++ b/test/tvp_test.exs @@ -0,0 +1,36 @@ +defmodule TvpTest do + import Tds.TestHelper + require Logger + use ExUnit.Case, async: true + alias Tds.Parameter + + @tag timeout: 50000 + + setup do + opts = Application.fetch_env!(:tds, :opts) + {:ok, pid} = Tds.start_link(opts) + + {:ok, [pid: pid]} + end + + test "TVP in stored proc", context do + assert :ok = query("BEGIN TRY DROP PROCEDURE __tvpTest DROP TYPE TvpTestType END TRY BEGIN CATCH END CATCH", []) + assert :ok = query(""" + CREATE TYPE TvpTestType AS TABLE ( + d int + ); + """, []) + assert :ok = query(""" + CREATE PROCEDURE __tvpTest (@tvp TvpTestType readonly) + AS BEGIN + select * from @tvp + END + """, []) + + params = [ + %Parameter{name: "@tvp", value: %{name: "TvpTestType", columns: [%Parameter{name: "d", type: :integer, value: nil}], rows: [[1]]}, type: :tvp} + ] + + assert {:ok, res} = query("__tvpTest @tvp", params) + end +end From cff49d7fa3b8c9bdddbb629e4fcb90e55bfde86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Fri, 10 Nov 2017 12:37:46 +0100 Subject: [PATCH 06/11] new proc function Adds support for using a provided stored proc as procedure name instead of default :sp_execute --- lib/tds.ex | 5 +++++ lib/tds/messages.ex | 5 +++++ lib/tds/protocol.ex | 16 ++++++++++++++++ test/test_helper.exs | 2 +- test/tvp_test.exs | 2 +- 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/tds.ex b/lib/tds.ex index 0179e29..1b4dced 100644 --- a/lib/tds.ex +++ b/lib/tds.ex @@ -17,6 +17,11 @@ defmodule Tds do end end + def proc(pid, statement, params, opts \\ []) do + opts = Keyword.put_new(opts, :proc, statement) + query(pid, statement, params, opts) + end + def query!(pid, statement, params, opts \\ []) do query = %Query{statement: statement} opts = Keyword.put_new(opts, :parameters, params) diff --git a/lib/tds/messages.ex b/lib/tds/messages.ex index 3c9107b..79a67c9 100644 --- a/lib/tds/messages.ex +++ b/lib/tds/messages.ex @@ -362,6 +362,11 @@ defmodule Tds.Messages do defp encode_rpc(:sp_unprepare, params) do <<0xFF, 0xFF, @tds_sp_unprepare::little-size(2)-unit(8), 0x00, 0x00>> <> encode_rpc_params(params, "") end + defp encode_rpc(proc, params) when is_binary(proc) do + rpc_size = byte_size(proc) + rpc_name = to_little_ucs2(proc) + <> <> rpc_name <> <<0x00, 0x00>> <> encode_rpc_params(params, "") + end # Finished processing params defp encode_rpc_params([], ret), do: ret diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 907a87b..8226fd4 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -90,6 +90,7 @@ defmodule Tds.Protocol do def handle_execute(%Query{statement: statement} = query, params, opts, %{sock: _sock} = s) do params = opts[:parameters] || params + s = if opts[:proc], do: Map.put_new(s, :proc, opts[:proc]), else: s if params != [] do send_param_query(query, params, s) @@ -391,6 +392,21 @@ defmodule Tds.Protocol do # {:ok, %{s | statement: nil, state: :ready}} #end + def send_param_query(%Query{handle: handle} = _query, params, %{proc: proc} = s) do + params = Tds.Parameter.prepare_params(params) + # msg = msg_rpc(proc: :sp_executesql, params: params) + msg = msg_rpc(proc: proc, params: params) + + case msg_send(msg, s) do + {:ok, %{result: result} = s} -> + {:ok, result, %{s | state: :ready}} + {:error, err, %{transaction: :started} = s} -> + {:error, err, %{s | transaction: :failed}} + err -> + err + end + end + def send_param_query(%Query{handle: handle} = _query, params, %{transaction: :started} = s) do params = [ %Tds.Parameter{name: "@handle", type: :integer, direction: :input, value: handle} diff --git a/test/test_helper.exs b/test/test_helper.exs index c2b5009..3d13967 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -14,7 +14,7 @@ defmodule Tds.TestHelper do defmacro proc(proc, params, opts \\ []) do quote do - case Tds.Connection.proc(var!(context)[:pid], unquote(proc), + case Tds.proc(var!(context)[:pid], unquote(proc), unquote(params), unquote(opts)) do {:ok, %Tds.Result{rows: nil}} -> :ok {:ok, %Tds.Result{rows: []}} -> :ok diff --git a/test/tvp_test.exs b/test/tvp_test.exs index d97843b..8f7a560 100644 --- a/test/tvp_test.exs +++ b/test/tvp_test.exs @@ -31,6 +31,6 @@ defmodule TvpTest do %Parameter{name: "@tvp", value: %{name: "TvpTestType", columns: [%Parameter{name: "d", type: :integer, value: nil}], rows: [[1]]}, type: :tvp} ] - assert {:ok, res} = query("__tvpTest @tvp", params) + assert [[1]] = proc("__tvpTest", params) end end From d84ab6a73ea6527fbed2dc017d4f9183b767e93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Fri, 10 Nov 2017 12:42:23 +0100 Subject: [PATCH 07/11] fix! use correct map key in tvp param descriptor --- lib/tds/types.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tds/types.ex b/lib/tds/types.ex index dd2cf07..3346892 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -771,7 +771,7 @@ defmodule Tds.Types do :date -> "date" :time -> "time" :smalldatetime -> "smalldatetime" - :tvp -> "#{value.vame} readonly" + :tvp -> "#{value.name} readonly" :binary -> encode_binary_descriptor(value) :string -> cond do From b663074f1a292f537ab046f1b03bdaa541e6f22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Fri, 10 Nov 2017 12:55:32 +0100 Subject: [PATCH 08/11] remove tvp row temp variable --- lib/tds/types.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tds/types.ex b/lib/tds/types.ex index 3346892..a0dc0c8 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -994,7 +994,7 @@ defmodule Tds.Types do row_bin = column_attrs |> Enum.zip(params) |> Enum.reduce(<<>>, fn ({{type, attr}, param}, acc) -> - foo = acc <> encode_data(type, param, attr) + acc <> encode_data(type, param, attr) end) row_acc <> << 0x01 >> <> row_bin end) From 55ec02c6fa7d51f858fe6c6921754607384508ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Fri, 10 Nov 2017 14:49:32 +0100 Subject: [PATCH 09/11] delimit column metadata and row data --- lib/tds/types.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/tds/types.ex b/lib/tds/types.ex index a0dc0c8..4090bdc 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -985,21 +985,23 @@ defmodule Tds.Types do column_length = <> {column_attrs, column_meta} = Enum.reduce(columns, {[], <<>>}, fn (%Parameter{} = param, {attrs, acc_bin}) -> {bin_type, data, attr} = encode_data_type(param) - bin = acc_bin <> <<0x00 :: little-unsigned-32, 0x00 :: little-unsigned-16 >> <> data <> <<0, 0>> + bin = acc_bin <> <<0x00 :: little-unsigned-32, 0x00 :: little-unsigned-16 >> <> data <> <<0x00>> {[{bin_type, attr} | attrs], bin} end) row_data = Enum.reduce(rows, <<>>, fn (params, row_acc) -> row_bin = column_attrs + |> Enum.reverse |> Enum.zip(params) |> Enum.reduce(<<>>, fn ({{type, attr}, param}, acc) -> acc <> encode_data(type, param, attr) end) + row_acc <> << 0x01 >> <> row_bin end) - column_length <> column_meta <> row_data <> <<0x00>> + column_length <> column_meta <> <<0x00>> <> row_data <> <<0x00>> end @doc """ From 799e8761f8d2f830adf272d50369ce0e81599d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Mon, 13 Nov 2017 16:29:11 +0100 Subject: [PATCH 10/11] wip! tvp columns --- lib/tds/column.ex | 9 +++++++ lib/tds/types.ex | 62 +++++++++++++++++++++++++++++++++++++++++++++-- test/tvp_test.exs | 25 ++++++++++++++++--- 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 lib/tds/column.ex diff --git a/lib/tds/column.ex b/lib/tds/column.ex new file mode 100644 index 0000000..9573f30 --- /dev/null +++ b/lib/tds/column.ex @@ -0,0 +1,9 @@ +defmodule Tds.Column do + @type t :: %__MODULE__{ + name: String.t | nil, + type: Atom | nil, + opts: Keyword.t + } + + defstruct [name: "", type: nil, opts: []] +end diff --git a/lib/tds/types.ex b/lib/tds/types.ex index 4090bdc..c86b95a 100644 --- a/lib/tds/types.ex +++ b/lib/tds/types.ex @@ -4,6 +4,7 @@ defmodule Tds.Types do use Bitwise alias Tds.Parameter + alias Tds.Column alias Tds.DateTime alias Tds.DateTime2 @@ -983,8 +984,8 @@ defmodule Tds.Types do def encode_data(@tds_data_type_tvp, %{columns: columns, rows: rows}, _attrs) do column_length = <> - {column_attrs, column_meta} = Enum.reduce(columns, {[], <<>>}, fn (%Parameter{} = param, {attrs, acc_bin}) -> - {bin_type, data, attr} = encode_data_type(param) + {column_attrs, column_meta} = Enum.reduce(columns, {[], <<>>}, fn (%Column{} = param, {attrs, acc_bin}) -> + {bin_type, data, attr} = encode_column_type(param) bin = acc_bin <> <<0x00 :: little-unsigned-32, 0x00 :: little-unsigned-16 >> <> data <> <<0x00>> {[{bin_type, attr} | attrs], bin} @@ -1004,9 +1005,66 @@ defmodule Tds.Types do column_length <> column_meta <> <<0x00>> <> row_data <> <<0x00>> end + def encode_column_type(%Column{type: type} = col) when type != nil do + case type do + :varchar -> encode_bigvarchar_col_type(col) + :boolean -> encode_binary_type(%Parameter{type: :boolean, value: nil}) + :varbinary -> encode_varbinary_col_type(col) + :int -> encode_integer_type(%Parameter{type: :integer, value: nil}) + :decimal -> encode_decimal_type(%Parameter{type: :decimal, value: nil}) + :float -> encode_float_type(%Parameter{type: :float, value: nil}) + :datetime -> encode_datetime_type(%Parameter{type: :datetime, value: nil}) + :smalldatetime -> encode_smalldatetime_type(%Parameter{type: :smalldatetime, value: nil}) + :datetime2 -> encode_datetime2_type(%Parameter{type: :datetime2, value: nil}) + :datetimeoffset -> encode_datetimeoffset_type(%Parameter{type: :datetimeoffset, value: nil}) + :date -> encode_date_type(%Parameter{type: :datetimeoffset, value: nil}) + :time -> encode_time_type(%Parameter{type: :time, value: nil}) + :uuid -> encode_uuid_type(%Parameter{type: :uuid, value: nil}) + end + end + + def encode_bigvarchar_col_type(%Column{opts: opts} = col) do + type = @tds_data_type_bigvarchar + length = Keyword.get(opts, :length, 0) + collation = <<0x00, 0x00, 0x00, 0x00, 0x00>> + bin_length = if length <= 8000, do: <<8000::little-unsigned-16>>, else: <<0xFF, 0xFF>> + data = <> <> bin_length <> collation + + {type, data, length: length} + end + + def encode_varbinary_col_type(%Column{opts: opts} = col) do + type = @tds_data_type_bigvarbinary + length = Keyword.get(opts, :length, 0) + bin_length = if length <= 8000, do: <<8000::little-unsigned-16>>, else: <<0xFF, 0xFF>> + data = <> <> bin_length + + {type, data, length: length} + end + @doc """ Data Encoding String Types """ + + def encode_data(@tds_data_type_bigvarchar, nil, opts) do + length = Keyword.get(opts, :length, 0) + if length <= 8000, + do: <<65535::little-unsigned-16>>, + else: <<@tds_plp_null::little-unsigned-64>> + end + + def encode_data(@tds_data_type_bigvarchar, value, opts) do + value_size = byte_size(value) + cond do + value_size <= 0 -> + <<0x00::unsigned-64, 0x00::unsigned-32>> + value_size > 8000 -> + encode_plp(value) + true -> + <> <> value + end + end + def encode_data(@tds_data_type_nvarchar, nil, _), do: <<@tds_plp_null::little-unsigned-64>> def encode_data(@tds_data_type_nvarchar, value, _) do diff --git a/test/tvp_test.exs b/test/tvp_test.exs index 8f7a560..0d3d9f7 100644 --- a/test/tvp_test.exs +++ b/test/tvp_test.exs @@ -3,6 +3,7 @@ defmodule TvpTest do require Logger use ExUnit.Case, async: true alias Tds.Parameter + alias Tds.Column @tag timeout: 50000 @@ -17,9 +18,13 @@ defmodule TvpTest do assert :ok = query("BEGIN TRY DROP PROCEDURE __tvpTest DROP TYPE TvpTestType END TRY BEGIN CATCH END CATCH", []) assert :ok = query(""" CREATE TYPE TvpTestType AS TABLE ( - d int + a int, + b uniqueidentifier, + c varchar(100), + d varbinary(max) ); """, []) + assert :ok = query(""" CREATE PROCEDURE __tvpTest (@tvp TvpTestType readonly) AS BEGIN @@ -27,10 +32,24 @@ defmodule TvpTest do END """, []) + rows = [1, <<158, 3, 157, 56, 133, 56, 73, 67, 128, 121, 126, 204, 115, 227, 162, 157>>, "foo", "{\"foo\":\"bar\",\"baz\":\"biz\"}"] params = [ - %Parameter{name: "@tvp", value: %{name: "TvpTestType", columns: [%Parameter{name: "d", type: :integer, value: nil}], rows: [[1]]}, type: :tvp} + %Parameter{ + name: "@tvp", + value: %{ + name: "TvpTestType", + columns: [ + %Column{name: "a", type: :int}, + %Column{name: "b", type: :uuid}, + %Column{name: "c", type: :varchar}, + %Column{name: "d", type: :varbinary}, + ], + rows: [rows] + }, + type: :tvp + } ] - assert [[1]] = proc("__tvpTest", params) + assert [^rows] = proc("__tvpTest", params) end end From c6f3de81551216b3a287383151fea54051123f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Th=C3=B6rnqvist?= Date: Fri, 17 Nov 2017 10:50:33 +0100 Subject: [PATCH 11/11] send_proc fn --- lib/tds/protocol.ex | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 8226fd4..5b0eed9 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -90,12 +90,12 @@ defmodule Tds.Protocol do def handle_execute(%Query{statement: statement} = query, params, opts, %{sock: _sock} = s) do params = opts[:parameters] || params - s = if opts[:proc], do: Map.put_new(s, :proc, opts[:proc]), else: s + proc = opts[:proc] || nil - if params != [] do - send_param_query(query, params, s) - else - send_query(statement, s) + cond do + params != [] and is_nil(proc) -> send_param_query(query, params, s) + not is_nil(proc) -> send_proc(proc, params, s) + true -> send_query(statement, s) end end @@ -392,9 +392,8 @@ defmodule Tds.Protocol do # {:ok, %{s | statement: nil, state: :ready}} #end - def send_param_query(%Query{handle: handle} = _query, params, %{proc: proc} = s) do + def send_proc(proc, params, s) do params = Tds.Parameter.prepare_params(params) - # msg = msg_rpc(proc: :sp_executesql, params: params) msg = msg_rpc(proc: proc, params: params) case msg_send(msg, s) do