Interpolated strings and SQL

Timon Gehr timon.gehr at gmx.ch
Tue Jan 9 12:35:41 UTC 2024


On 1/9/24 00:06, Walter Bright wrote:
> Here's how SQL support is done for DIP1036:
> 
> https://github.com/adamdruppe/interpolation-examples/blob/master/lib/sql.d
> 
> ```
> auto execi(Args...)(Sqlite db, InterpolationHeader header, Args args, 
> InterpolationFooter footer) {
>      import arsd.sqlite;
> 
>      // sqlite lets you do ?1, ?2, etc
> 
>      enum string query = () {
>          string sql;
>          int number;
>          import std.conv;
>          foreach(idx, arg; Args)
>              static if(is(arg == InterpolatedLiteral!str, string str))
>                  sql ~= str;
>              else static if(is(arg == InterpolationHeader) || is(arg == 
> InterpolationFooter))
>                  throw new Exception("Nested interpolation not supported");
>              else static if(is(arg == InterpolatedExpression!code, 
> string code))
>                  {   } // just skip it
>              else
>                  sql ~= "?" ~ to!string(++number);
>          return sql;
>      }();
> 
>      auto statement = Statement(db, query);
>      int number;
>      foreach(arg; args) {
>          static if(!isInterpolatedMetadata!(typeof(arg)))
>              statement.bind(++number, arg);
>      }
> 
>      return statement.execute();
> }
> ```
> This:
> 
> 1. The istring, after converted to a tuple of arguments, is passed to 
> the `execi` template.
> 2. It loops over the arguments, essentially turing it (ironically!) back 
> into a format

This is not ironic at all. The point is it _can_ do that, while DIP1027 
_cannot_ do _either this or the opposite direction_. It is yourself who 
called the istring the building block instead of the end product, but 
now you are indeed failing to turn the sausage back into the cow.

> string. The formats, instead of %s, are ?1, ?2, ?3, etc.
> 3. It skips all the Interpolation arguments inserted by DIP1036.
> 4. The remaining argument are each bound to the indices 1, 2, 3, ...
> 5. Then it executes the sql statement.
> 
> Note that nested istrings are not supported.
> ...

But you get a useful error message that exactly pinpoints what the 
problem is. Also, they could be supported, which is the point.

> Let's see how this can work with DIP1027:
> 
> ```
> auto execi(Args...)(Sqlite db, Args args) {
>      import arsd.sqlite;
> 
>      // sqlite lets you do ?1, ?2, etc
> 
>      enum string query = () {
>          string sql;
>          int number;
>          import std.conv;
>          auto fmt = arg[0];
>          for (size_t i = 0; i < fmt.length, ++i)
>          {
>              char c = fmt[i];
>              if (c == '%' && i + 1 < fmt.length && fmt[i + 1] == 's')
>              {
>                  sql ~= "?" ~ to!string(++number);
>                  ++i;
>              }
>              else if (c == '%' && i + 1 < fmt.length && fmt[i + 1] == '%')
>                  ++i;  // skip escaped %
>              else
>                  sql ~= c;
>          }
>          return sql;
>      }();
> 
>      auto statement = Statement(db, query);
>      int number;
>      foreach(arg; args[1 .. args.length]) {
>          statement.bind(++number, arg);
>      }
> 
>      return statement.execute();
> }
> ```
> This:
> ...

This does not work.

> 1. The istring, after converted to a tuple of arguments, is passed to 
> the `execi` template.
> 2. The first tuple element is the format string.
> 3. A replacement format string is created by replacing all instances of 
> "%s" with
> "?n", where `n` is the index of the corresponding arg.
> 4. The replacement format string is bound to `statement`, and the 
> arguments are bound
> to their indices.
> 5. Then it executes the sql statement.
> 
> It is equivalent.

No. As Nickolay already explained, it is not equivalent.

- It does not even compile, even if we fix the typo arg -> args.

That is enough to dismiss DIP1027 for this example. However, let's for 
the sake of argument assume that, miraculously, `execi` can read the 
format string at compile time, then:

- With this signature, if you pass a manually-constructed string to it, 
it would just accept the SQL injection.
- It does not give a proper error message for nested istrings.
- It has to manually parse the format string. It iterates over each 
character of the original format string.
- It (ironically!) constructs a new format string, the original one was 
useless.
- If you pass a bad format string to it (for example, by specifying a 
manual format), it will just do nonsense, while DIP1036e avoids bad 
format strings by construction.


More information about the Digitalmars-d mailing list