3

I have just recently began using OPENROWSET to insert images into a table. Previously, I would specify the path to each image (1 image = 1 INSERT statement), and use PHP to generate the image's binary string:

INSERT INTO nopCommerce..Picture (PictureBinary, MimeType, SeoFilename, AltAttribute, TitleAttribute, IsNew)
VALUES (
    (
        SELECT *
        FROM OPENROWSET(BULK '" . $this->image['path'] . "', SINGLE_BLOB) AS Binary
    ),
    '" . $this->image['mime_type'] . "',
    '" . $this->image['seo_filename'] . "',
    '" . $this->image['alt'] . "',
    '',
    0
)

However, I am trying to insert all images with a single query. So, I have began storing the path to each image into a table, and now I need to insert each one as I did before (just using the table's path field instead of a PHP string). But, when I attempt the following:

INSERT INTO nopCommerce..Picture (PictureBinary, MimeType, SeoFilename, AltAttribute, TitleAttribute, IsNew)
SELECT
    (
        SELECT *
        FROM OPENROWSET(BULK ImagePath, SINGLE_BLOB) AS Binary
    ),
    MimeType,
    Name,
    Alt,
    '',
    0
FROM nopRMS..Product_Image_Mappings

I receive the following error:

Msg 102, Level 15, State 1, Line 5
Incorrect syntax near 'ImagePath'.

So, I tried adding quotes around the column's name (to no avail):

INSERT INTO nopCommerce..Picture (PictureBinary, MimeType, SeoFilename, AltAttribute, TitleAttribute, IsNew)
SELECT
    (
        SELECT *
        FROM OPENROWSET(BULK 'ImagePath', SINGLE_BLOB) AS Binary
    ),
    MimeType,
    Name,
    Alt,
    '',
    0
FROM nopRMS..Product_Image_Mappings

Msg 4860, Level 16, State 1, Line 1
Cannot bulk load. The file "ImagePath" does not exist.

There has to be a way to accomplish this, I just cannot find the proper syntax online anywhere. Does anyone know how to tell SQL Server to get the path (string) from dbo.Product_Image_Mappings.ImagePath?

UPDATE
I forgot to give you an example of a value that dbo.Product_Image_Mappings.ImagePath would return. It's paths like \\DEREK\WebImages\1\ca-82300.jpg...

UPDATE
It appears that Eirikur Eiriksson has provided a solution in this thread, but this looks like an overly-complicated method of achieving the same end...

UPDATE (Attempt Using Eirikur Eiriksson's Method)

DECLARE @SQL_STR NVARCHAR(MAX) = N'';

SELECT @SQL_STR = STUFF(
    (
        SELECT 
            N'
            UNION ALL
            SELECT '
            + N'(SELECT X.BulkColumn FROM OPENROWSET(BULK '
            + NCHAR(39) + im.ImagePath + NCHAR(39)
            + N', SINGLE_BLOB) AS X) AS PictureBinary,'
            + NCHAR(39) + im.MimeType + NCHAR(39)
            + N' AS MimeType,'
            + NCHAR(39) + im.Name + NCHAR(39)
            + N' AS SeoFilename,'
            + NCHAR(39) + REPLACE(im.Alt, '''', '''''') + NCHAR(39)
            + N' AS AltAttribute,'
            + N'NULL AS TitleAttribute,'
            + N'0 AS IsNew'
        FROM nopRMS..Product_Image_Mappings im
        FOR XML PATH(''), TYPE
    ).value('.[1]','NVARCHAR(MAX)'),1,12,N''
)

INSERT INTO nopCommerce..Picture (PictureBinary, MimeType, SeoFilename, AltAttribute, TitleAttribute, IsNew)
EXEC (@SQL_STR);

This kinda worked, but it only inserted 42 rows (out of 7200+)... I need this to be 100% accurate :( I admit though, I may need to change something about this query, but I don't know anything about it (aside from the basic INSERT, SELECT, etc.)

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
Derek Foulk
  • 1,892
  • 1
  • 19
  • 37
  • I should also mention that I am using this insert statement inside of a merge statement. I believe this may limit my options as well (because it looks like Dynamic SQL is the only option, and I don't believe that I can execute Dynamic SQL inside of `WHEN NOT MATCHED BY TARGET`. – Derek Foulk Apr 14 '16 at 17:50
  • FYI: I updated the formatting a little in the Question, but also the name of the person who posted that code on the SQL Server Central forum. The term "SSCertifiable" is not a name but a label associated with how many points and/or logins to the site one has. – Solomon Rutzky Apr 14 '16 at 21:32
  • You have PHP code in your first code sample. Are you building SQL inside a PHP page and running it? It's important to know. Your second code attempt (using 'ImagePath') might be achievable using a cursor - a cursor builds the full dynamic string for every record and executes it, but it'd be slow. – Nick.Mc Apr 15 '16 at 00:11
  • @Nick.McDermaid - Yes, I have been constructing the query in PHP then executing it with the SQLSRV drivers. TBH, speed is not necessarily a concern here. This app is a CLI app that runs every X minutes to synchronize our point of sale and our web app. So, as long as it takes less than 5-10 minutes to execute, it'd work just fine. I've heard of cursors, but I do not know how to use them with this driver... Can you elaborate on what is possible with cursors and my question please? Thanks! – Derek Foulk Apr 15 '16 at 21:28
  • **Regarding the Erik-Update**: Not quite sure exactly what the point of the extra complication of FOR XML is about. I'd just build a sql-string and EXEC it. But my first impression about the "42 rows (out of 7200+)..." is that only 42 of the rows in nopRMS..Product_Image_Mappings had all non-null values for the columns you used. One null in the string concatenation will wipe out that entire statement in the overall query you're building since CONCAT_NULL_YIELDS_NULL is normally on. COALESCE() is your friend. – Granger Nov 18 '16 at 03:16

1 Answers1

1

Maybe don't use OPENROWSET? What you are wanting can be handled in a much simpler and cleaner manner using SQLCLR. You can create a Scalar UDF to just read the contents of a file and return that as a VARBINARY(MAX). Then it will fit in nicely to your existing query. For example:

INSERT INTO nopCommerce.dbo.Picture (PictureBinary, MimeType, SeoFilename, 
                      AltAttribute, TitleAttribute, IsNew)
SELECT
    dbo.GetBinaryFile([ImagePath]) AS [PictureBinary],
    MimeType,
    Name,
    Alt,
    '',
    0
FROM nopRMS.dbo.Product_Image_Mappings;

And how much code does it take for dbo.GetBinaryFile()? Here it is:

using System;
using System.Data.SqlTypes;
using System.IO;
using Microsoft.SqlServer.Server;

[return:SqlFacet(MaxSize = -1)]
[SqlFunction(IsDeterministic = false, IsPrecise = true)]
public static SqlBytes GetBinaryFile([SqlFacet(MaxSize = 1000)] SqlString FilePath)
{
    if (FilePath.Value.Trim().Equals(String.Empty))
    {
        return SqlBytes.Null;
    }

    return new SqlBytes(File.ReadAllBytes(FilePath.Value));
}

And the T-SQL wrapper object is the following (please note the WITH RETURNS NULL ON NULL INPUT line as it skips execution if NULL is passed in, hence no need to check for FilePath.IsNull in the C# code :-)

CREATE FUNCTION [dbo].[GetBinaryFile](@FilePath NVARCHAR(1000))
RETURNS VARBINARY(MAX)
WITH RETURNS NULL ON NULL INPUT
AS
 EXTERNAL NAME [CSVParser].[CSVUtils].[GetBinaryFile];

The Assembly will need to be marked as WITH PERMISSION_SET = EXTERNAL_ACCESS. Many people go the easy route of setting the database property of TRUSTWORTHY to ON in order to accomplish this, but that is a security risk and isn't even necessary. Just do the following and you can set the Assembly to EXTERNAL_ACCESS while keeping TRUSTWORTHY set to OFF:

  1. Sign the Assembly.
  2. Create an Asymmetric Key in master from that DLL.
  3. Create a Login (also in master) from that Asymmetric Key.
  4. Grant the new Login the EXTERNAL ACCESS ASSEMBLY permission.

You can find detailed instructions on how to do this in Visual Studio / SSDT (SQL Server Data Tools) in the following article that I wrote: Stairway to SQLCLR Level 7: Development and Security (that site does require free registration in order to view the content).

Also, for anyone that does not want to bother with creating / deploying the Assembly, a similar function is available (though not for free) in the Full version of the SQL# library (which I created, and while many functions are free, the File_* file system functions are only in the Full version).

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171