You are not logged in.
Nice! Well done Arnaud!
Also, note that AutoFlushTimeOut does not work when you are in Debug mode (DebugHook = 1).
FYI, it works for me when I tried it (I have not committed the latest patch so I still manually added the trailing slash).
with TMyLog.Family do
begin
Level := [sllInfo, sllError, sllServer];
DestinationPath := ExtractFilePath(Application.ExeName) + 'logs\';
end;
TMyLog.Add.Log(sllInfo, 'Testing');
Question for @win2014: Does the logs folder exist?
Just wondering if anyone has had time to test it out? Thank you.
Here's a sample console code:
program TestLogger;
{$APPTYPE CONSOLE}
{$R *.res}
uses
SynCommons,
System.SysUtils;
type
TTestLogger = class(TSynLog)
procedure ComputeFileName; override;
end;
TSynLogFileEcho = class(TSynLogFile)
public
function LogEcho(Sender: TTextWriter; Level: TSynLogInfo; const Text: RawUTF8): boolean;
end;
var
LogFile: TSynLogFileEcho;
FEventCaption: array[TSynLogInfo] of string;
E: TSynLogInfo;
{ TTestLogger }
procedure TTestLogger.ComputeFileName;
begin
inherited;
fFileName := 'abc.log';
end;
{ TSynLogFileEcho }
function TSynLogFileEcho.LogEcho(Sender: TTextWriter; Level: TSynLogInfo;
const Text: RawUTF8): boolean;
var
P: PUTF8Char;
Line: RawUTF8;
begin
P := Pointer(Text);
repeat
Line := GetNextLine(P, P);
if Length(Line) < 24 then
Continue;
Self.AddInMemoryLine(Line);
until P = nil;
Result := True;
end;
procedure ShowLogContent;
var
index: Integer;
begin
for index := LogFile.Count - 1 downto 0 do
WriteLn('Level: ', FEventCaption[LogFile.EventLevel[index]], ' Text: ', LogFile.EventText[index]);
end;
procedure WaitForEnter(const Msg: string);
begin
WriteLn(Msg);
Write('Press ENTER to continue...');
ReadLn;
end;
begin
try
with TTestLogger.Family do
begin
Level := LOG_VERBOSE;
FileExistsAction := acAppend;
end;
for E := succ(sllNone) to high(E) do
FEventCaption[E] := GetCaptionFromEnum(TypeInfo(TSynLogInfo),ord(E));
if FileExists('abc.log') then
LogFile := TSynLogFileEcho.Create('abc.log')
else LogFile := TSynLogFileEcho.Create;
WaitForEnter('Listing initial content');
ShowLogContent;
TTestLogger.Add.Log(sllNewRun, 'Starting Up!');
TTestLogger.Family.EchoRemoteStart(nil, LogFile.LogEcho, False);
WaitForEnter('After first log');
ShowLogContent;
TTestLogger.Add.Log(sllInfo, 'Information, information, information');
TTestLogger.Add.Log(sllDebug, 'Debug, debug, debug');
TTestLogger.Add.Log(sllWarning, 'Warning, warning, warning');
WaitForEnter('After sample logs');
ShowLogContent;
WaitForEnter('The End');
LogFile.Free;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
The first time you run it, echo would be correct.
The second time you run it, the log text from the log file will be dumped correctly, but subsequent echoes will show blank text.
The third time you run it, log text from first and second run will be shown, but subsequent echoes will also show blank text.
Yup, weirdly LogView works. However, in my sample code, it's exactly the same thing, just without the remote connection.
The message was added via TSynLog.Add.Log(slxxx, 'xxx'); so I assume the format should be correct? When debugging, I see that it returns an empty text because strlen always return 0. However, when I restart the test app (it first works by loading an existing log file, before doing the in-memory append for subsequent logs), the same looping code above works, so I believe the format is correct.
I had this issue much earlier and managed to resolve it by appending a #13 to the Line I got from LogEcho, however that has stopped working since a few patches ago... no idea why yet though.
Hi Arnaud,
If I read back the line that has been added via AddInMemoryLine, I will get an empty string. EventLevel will return the correct event level though? The line sent to AddInMemoryLine is retrieved via a TOnTextWriterEcho event.
Some code snippet:
function TTestLogger.LogEcho(Sender: TTextWriter; Level: TSynLogInfo;
const Text: RawUTF8): boolean;
var
P: PUTF8Char;
Line: RawUTF8;
begin
repeat
Line := GetNextLine(P, P);
if Length(Line) < 24 then
Continue;
fActiveLog.AddInMemoryLine(Line);
until P = nil;
end;
// and when I do this:
for index := fActiveLog.Count - 1 downto 0 do
WriteLn('Level: ', EventLevelCaption[fActiveLog.EventLevel[index]], ' Text: ', fActiveLog.EventText[index]);
// I get an empty string for Event Text, but the Event Level is correct
Any ideas? Or did I do something wrongly?
Thanks for the reply Arnaud.
I wonder if the built-in SQLite is usable enough to store such data for now, since I'm not having that much traffic? Of course, that means having to do my own presentation layer to display the stats.
I've been meaning to track and store server access stats.
Something like, a per-second snapshot of total calls to each Method/Interface of each TSQLRestServer. Which then can be used to create 5 min, 30 min, hourly and daily totals, for charting purposes.
What would be a good way to collect and persist this data, without affecting performance too much? I have toyed with the idea of having a thread to send data from TSQLRestServerStats to a Redis server (say, every second), but it would be great if this can all be done in a self-contained manner, with no extra dependencies. Would storing it in SQLite create too big a performance hit? Will it bulk up the size of the database too much? (I'm looking at, say, an median of 10-20 calls per minute, up to a max of 200-300 calls per second).
Anyone has experience doing this, in conjuction with the mORMot Framework? Thanks!
Wow this is great news!
OK, I'll remember to lock it if I need to use it from multiple threads. Thanks Arnaud!
Hi Arnaud,
I do not see any locking in the procedure, so I'm wondering if it is as thread-safe as its disk-based counterpart, TSynLog.LogInternal?
Thanks!
Line 42774 also needs amendment to
if (fFileRotationSize>0) or (fFamily.FileExistsAction <> acOverwrite) then
There is a bug that prevents log file appends from working correctly... it ends up overwriting the log file nevertheless if RotationSize is not set (Line 42753), which is why I proposed by code above.
To cater for your updated log file access retry code, the code can be updated to be as follows:
for retry := 0 to 2 do begin
for i := 1 to 10 do
try
if FileExists(fFileName) and (fFamily.FileExistsAction <> acOverwrite) then
begin
if fFamily.FileExistsAction = acAppend then
Include(fInternalFlags,logHeaderWritten);
end else if (fFileRotationSize=0) or not FileExists(fFileName) then
TFileStream.Create(fFileName,fmCreate).Free; // create a void file
fWriterStream := TFileStream.Create(fFileName,
fmOpenReadWrite or fmShareDenyWrite); // open with read sharing
break;
except
on Exception do
Sleep(100);
end;
if fWriterStream<>nil then
break;
fFileName := ChangeFileExt(fFileName,'-'+fFamily.fDefaultExtension);
end;
Lines 42742 to 42748 are unnecessary.
Thanks Arnaud!
Hi Arnaud,
Would it make more sense to move the call to DeleteFile(fFileName) into the IF block before it?
The reason being that if the OnRotate event was handled (True was returned), it most likely means the programmer is saying "OK I have everything under control".
Also, would appreciate it if you could take a look at http://synopse.info/forum/viewtopic.php?id=2022
Thank you!
Why not access it this way?
var
Entries: TEntryDB;
Entries := TEntryDB.CreateAndFillPrepare(FDatabase, yourQueryHere);
while Entries.FillOne do
begin
Display(Entries.SomeProperty);
end;
Some code would be useful
Hi Arnaud,
Would just like to know what do you think of the proposed changes.
If it should not go to the main code, then let me know and I'll subclass it myself. Thanks!
Additional updates (my previous patch was buggy and incomplete)...
procedure TSynLog.CreateLogWriter;
begin
if fWriterStream=nil then begin
GenerateFileName;
if fFamily.NoFile then
fWriterStream := TFakeWriterStream.Create else
begin
if FileExists(fFileName) and (fFamily.fLogExistsAction <> acOverwrite) then
begin
if fFamily.fLogExistsAction = acAppend then
Include(fInternalFlags,logHeaderWritten);
end else
if (fFileRotationSize=0) or not FileExists(fFileName) then
TFileStream.Create(fFileName,fmCreate).Free; // create a void file
fWriterStream := TFileStream.Create(fFileName, // open with read sharing
fmOpenReadWrite or fmShareDenyWrite);
end;
if (fFileRotationSize>0) or (fFamily.fLogExistsAction <> acOverwrite) then
fWriterStream.Seek(0,soFromEnd); // in rotation mode, append at the end
end;
if fWriterClass=nil then
fWriterClass := TTextWriter;
if fWriter=nil then
fWriter := fWriterClass.Create(fWriterStream,fFamily.BufferSize);
fWriter.EndOfLineCRLF := fFamily.EndOfLineCRLF;
if integer(fFamily.EchoToConsole)<>0 then
fWriter.EchoAdd(ConsoleEcho);
if Assigned(fFamily.EchoCustom) then
fWriter.EchoAdd(fFamily.EchoCustom);
if Assigned(fFamily.fEchoRemoteClient) then
fWriter.EchoAdd(fFamily.fEchoRemoteEvent);
end;
Hi Arnaud,
What do you think of this? (SynCommons.pas)
SynCommons.pas | 95 +++++++++++++++++++++++++++++++++++-----------------------
1 file changed, 57 insertions(+), 38 deletions(-)
diff --git a/SynCommons.pas b/SynCommons.pas
index d673641..3fec809 100644
--- a/SynCommons.pas
+++ b/SynCommons.pas
@@ -10705,6 +10705,7 @@ type
/// how stack trace shall be computed
TSynLogStackTraceUse = (stManualAndAPI,stOnlyAPI,stOnlyManual);
+ TSynLogExistsAction = (acOverwrite, acAppend, acAppendWithHeader);
{/ regroup several logs under an unique family name
- you should usualy use one family per application or per architectural
@@ -10762,6 +10763,7 @@ type
fRotateFileCount: cardinal;
fRotateFileSize: cardinal;
fRotateFileAtHour: integer;
+ fLogExistsAction: TSynLogExistsAction;
function CreateSynLog: TSynLog;
{$ifdef MSWINDOWS}
procedure SetAutoFlush(TimeOut: cardinal);
@@ -10945,6 +10947,8 @@ type
// be appended instead
// - TSynLogFile class and our LogView tool will handle both patterns
property EndOfLineCRLF: boolean read fEndOfLineCRLF write fEndOfLineCRLF;
+ // action to take if log file already exists. Defaults to append with header
+ property LogExistsAction: TSynLogExistsAction read fLogExistsAction write fLogExistsAction;
end;
/// thread-specific internal context used during logging
@@ -11025,12 +11029,12 @@ type
procedure CreateLogWriter; virtual;
{$ifndef DELPHI5OROLDER}
procedure LogInternal(Level: TSynLogInfo; TextFmt: PWinAnsiChar;
- const TextArgs: array of const; Instance: TObject); overload;
+ const TextArgs: array of const; Instance: TObject); overload;
{$endif}
procedure LogInternal(Level: TSynLogInfo; const Text: RawUTF8;
Instance: TObject; TextTruncateAtLength: integer); overload;
procedure LogInternal(Level: TSynLogInfo; aName: PWinAnsiChar;
- aTypeInfo: pointer; var aValue; Instance: TObject=nil); overload;
+ aTypeInfo: pointer; var aValue; Instance: TObject=nil); overload;
// any call to this method MUST call UnLock
function LogHeaderLock(Level: TSynLogInfo): boolean;
procedure LogTrailerUnLock(Level: TSynLogInfo); {$ifdef HASINLINE}inline;{$endif}
@@ -11048,6 +11052,7 @@ type
function Instance: TSynLog;
function ConsoleEcho(Sender: TTextWriter; Level: TSynLogInfo;
const Text: RawUTF8): boolean; virtual;
+ procedure GenerateFileName; virtual;
public
/// intialize for a TSynLog class instance
// - WARNING: not to be called directly! Use Enter or Add class function instead
@@ -41663,6 +41668,7 @@ begin
fExceptionIgnore := TList.Create;
fLevelStackTrace :=
[sllError,sllException,sllExceptionOS,sllFail,sllLastError,sllStackTrace];
+ fLogExistsAction := acAppendWithHeader;
end;
function TSynLogFamily.CreateSynLog: TSynLog;
@@ -42644,48 +42650,61 @@ begin
end;
end;
-procedure TSynLog.CreateLogWriter;
+procedure TSynLog.GenerateFileName;
{$ifndef MSWINDOWS}
var i: integer;
{$endif}
-var timeNow,hourRotate,timeBeforeRotate: TDateTime;
+var timeNow, hourRotate, timeBeforeRotate: TDateTime;
+begin
+ {$ifdef MSWINDOWS}
+ ExeVersionRetrieve;
+ fFileName := Ansi7ToString(ExeVersion.ProgramName);
+ if fFamily.IncludeComputerNameInFileName then
+ fFileName := fFileName+' ('+Ansi7ToString(ExeVersion.Host)+')';
+ {$else}
+ fFileName := ExtractFileName(ParamStr(0));
+ i := Pos('.',fFileName);
+ if i>0 then
+ SetLength(fFileName,i-1);
+ {$endif}
+ fFileRotationSize := 0;
+ if fFamily.fRotateFileCount>0 then begin
+ if fFamily.fRotateFileSize>0 then
+ fFileRotationSize := fFamily.fRotateFileSize shl 10; // size KB -> B
+ if fFamily.fRotateFileAtHour in [0..23] then begin
+ hourRotate := EncodeTime(fFamily.fRotateFileAtHour,0,0,0);
+ timeNow := Time;
+ if hourRotate<timeNow then
+ hourRotate := hourRotate+1; // trigger will be tomorrow
+ timeBeforeRotate := hourRotate-timeNow;
+ fFileRotationNextHour := GetTickCount64+trunc(timeBeforeRotate*MSecsPerDay);
+ end;
+ end;
+ if (fFileRotationSize=0) and (fFileRotationNextHour=0) then
+ fFileName := fFileName+' '+Ansi7ToString(NowToString(false));
+ {$ifdef MSWINDOWS}
+ if IsLibrary then
+ fFileName := fFileName+' '+ExtractFileName(GetModuleName(HInstance));
+ {$endif}
+ if fFamily.fPerThreadLog=ptOneFilePerThread then
+ fFileName := fFileName+' '+IntToString(GetCurrentThreadId);
+ fFileName := fFamily.fDestinationPath+fFileName+fFamily.fDefaultExtension;
+end;
+
+procedure TSynLog.CreateLogWriter;
begin
if fWriterStream=nil then begin
- {$ifdef MSWINDOWS}
- ExeVersionRetrieve;
- fFileName := Ansi7ToString(ExeVersion.ProgramName);
- if fFamily.IncludeComputerNameInFileName then
- fFileName := fFileName+' ('+Ansi7ToString(ExeVersion.Host)+')';
- {$else}
- fFileName := ExtractFileName(ParamStr(0));
- i := Pos('.',fFileName);
- if i>0 then
- SetLength(fFileName,i-1);
- {$endif}
- fFileRotationSize := 0;
- if fFamily.fRotateFileCount>0 then begin
- if fFamily.fRotateFileSize>0 then
- fFileRotationSize := fFamily.fRotateFileSize shl 10; // size KB -> B
- if fFamily.fRotateFileAtHour in [0..23] then begin
- hourRotate := EncodeTime(fFamily.fRotateFileAtHour,0,0,0);
- timeNow := Time;
- if hourRotate<timeNow then
- hourRotate := hourRotate+1; // trigger will be tomorrow
- timeBeforeRotate := hourRotate-timeNow;
- fFileRotationNextHour := GetTickCount64+trunc(timeBeforeRotate*MSecsPerDay);
- end;
- end;
- if (fFileRotationSize=0) and (fFileRotationNextHour=0) then
- fFileName := fFileName+' '+Ansi7ToString(NowToString(false));
- {$ifdef MSWINDOWS}
- if IsLibrary then
- fFileName := fFileName+' '+ExtractFileName(GetModuleName(HInstance));
- {$endif}
- if fFamily.fPerThreadLog=ptOneFilePerThread then
- fFileName := fFileName+' '+IntToString(GetCurrentThreadId);
- fFileName := fFamily.fDestinationPath+fFileName+fFamily.fDefaultExtension;
+ GenerateFileName;
if fFamily.NoFile then
- fWriterStream := TFakeWriterStream.Create else begin
+ fWriterStream := TFakeWriterStream.Create else
+ begin
+ if FileExists(fFileName) then
+ begin
+ if fFamily.fLogExistsAction = acOverwrite then
+ DeleteFile(fFileName)
+ else if fFamily.fLogExistsAction = acAppend then
+ Include(fInternalFlags,logHeaderWritten);
+ end;
if (fFileRotationSize=0) or not FileExists(fFileName) then
TFileStream.Create(fFileName,fmCreate).Free; // create a void file
fWriterStream := TFileStream.Create(fFileName, // open with read sharing
Alternatively, if this is not possible, then I will subclass TSynLog. Thank you!
Hi Arnaud,
Is it possible to have a callback event for SynLog that returns a custom filename? Either that, or move the filename generation routine into another procedure/function that is virtual and may be overridden by a descendant class?
This is useful if I want to only have a daily log, so if the programme gets restarted, logging may resume in the same file. Also, if the file already exists, perhaps logHeaderWritten should be set in fInternalFlags, to prevent writing of header again.
Thank you!
If you're using the built-in ORM, caching happens automatically (see Section 5.5 of the SAD pdf file) on the server side.
Not sure what you mean by "postponed" operations.
EDIT: Oops, looks like Arnaud replied first
Thanks Arnaud!
Hi Arnaud,
I'm just calling it from the browser via http://localhost:8080/admin/AdminServer/isRunning
Very simple to reproduce:
IAdminServer = interface(IInvokable)
['{E3097205-9424-427E-B6B4-D61E044CC6C3}']
function IsRunning: Boolean;
end;
TAdminServer = class(TInterfacedObject, IAdminServer)
public
function IsRunning: Boolean;
end;
function TAdminServer.IsRunning: Boolean;
begin
Result := True;
end;
// ---- snip ----
AdminModel := TSQLModel.Create([TSQLUser], 'admin');
AdminDB := TSQLRestServerDB.Create(AdminModel, 'admin.db', False);
AdminServer := TSQLHttpServer.Create('8080', [AdminDB], '+', useHttpApiRegisteringURI, 32, secNone);
AdminServer.AccessControlAllowOrigin := '*';
AdminDB.ServiceRegister(TAdminServer, [TypeInfo(IAdminServer)], sicShared);
Code fails in mORMot.pas line 27791 because Par is nil, and the if statement is trying to perform character matching.
If I amend the line to
if (Par<>nil) and ((Par^='[') or IdemPChar(Par,'%5B')) then
it still fails at line 27047 of mORMot.pas because as long as the parameter list is nil, it will exit with an error.
Hi Arnaud,
It currently seems impossible to support functions in interfaces that do not require parameters?
for instance
interface IServerInfo
function IsRunning: Boolean;
end;
will return an error if the caller does a /server/isRunning
However, if I add any parameters to the URL, it works... e.g. /server/isRunning?random=13348
I believe this is due to checks for parameter list <> nil
Could there be an exception to this check for occurrences where the function is not expecting any parameters?
Hi Arnaud,
When the HttpServer is instantiated with useHttpApiRegisteringURI, all initial roots get registered.
However, subsequent calls to AddServer does not check this value and will pass False to AddUrl's aRegisterURI parameter, thereby causing those roots to not be automatically registered.
Suggestion:
1. Store the value of the initial TSQLHttpServerOptions.
2. mORMotHttpServer.pas line 419
aServer.Model.Root,fPort,(aHttpServerSecurity=secSSL),fDomainName)<>NO_ERROR then
to
aServer.Model.Root,fPort,(aHttpServerSecurity=secSSL),fDomainName,useHttpApiRegisteringURI=fHttpServerOptions)<>NO_ERROR then
Thanks!
For those who are wondering, this feature has been added in.
Thanks Arnaud!
That was fast!
*Now waiting (im)patiently for Daily Rotation*
I guess you could make use of the FindFirstChangeNotification API call to notify you of changes to the file.
I reckon you would most likely need to create a new descendant of TSynLogFile that
1. has an additional parameter to LoadFromMap to allow you to specify an Offset to P instead of always pointing to fMap.Buffer (chances are you would need to duplicate most of the code from TMemoryMapText.LoadFromMap as well as TSynLogFile.LoadFromMap, and strip out the parts that perform initialization and parts that always read from fLines[0], replacing it with fLines[fCount])
2. upon notification of file change, calculate the size difference and pass that in to a call to the new LoadFromMap
Or you could open a request ticket and request for Arnaud to add the Offset into TMemoryMapText.LoadFromMap and its descendants as well, and modify the code to support subsequent calls to LoadFromMap
Rotation works too, if there is an event like OnArchive so that we can decide if we want to handle it (move it to a different folder, archive it with zip instead of SynLZ, etc). Would that be possible?
Hi Arnaud,
What is the best way to create a daily log file using TSynLog?
I am thinking of creating a descendant of TSynLog with the following changes:
1. Filename will include current date, perhaps in yyyymmdd format.
2. If the filename exists when creating log for the first time, append to it, otherwise create a new file.
3. Start a background thread that checks for date change, and when that happens, recreate a new fWriterStream.
Or is there already some built-in mechanism?
The reason I am looking for a daily log file is to make it easier to search for logs based on dates. So if the user wants to look for logs on a particular date, he just needs to open the file that corresponds to the date he wants using a log viewer.
My 2 cents, Arnaud... there are really quite a few scenarios I have encountered that require this time of 1-to-many relationships.
I do hope foreign keys and 1-to-many (with optional lazy loading) can rank higher in the wish list
Done. Did I kill the formatting? http://synopse.info/fossil/tktview/1da1 … 1c04285598
Thanks Arnaud. I'll update this thread if I managed to get any positive breakthrough.
Hi Arnaud,
I would like to propose that the method JSONBufferToXML (SynCommons.pas, line 34345) accepts an optional ArrayName parameter that can be passed to AddJSONToXML. At the same time, perhaps the ArrayName can default to the Model's name, and may be set by a property of TSQLModel?
Thank you!
Arnaud, is there any other information that I could provide to help you look into this issue? Or should I just abandon this thread?
Thank you!
Lol... sorry about the connection string part... was getting sleepy (it's almost 2am here).
Now DataCompatibility is in the connection string, but no changes. Forcing the column type to ftUTF8 returns the same hex dump, and BinToHex of the Column as blob returns the same dump.
As for the fProps.Free error, I had no idea that a TSQLDBStatement instance would prevent the Conn Properties from being freed... oops
By the way, when I try to free my TOleDBMSSQL2012ConnectionProperties, I get this error at fProps.Free.
First chance exception at $75C5C42D. Exception class EOleDBException with message 'OLEDB Error 80040E05 - Object was open.'.
This only occurs if I execute a stored procedure. If I execute other SQL commands (SELECT, INSERT, UPDATE), I do not get this error. Here's some sample code:
fProps := TOleDBMSSQLConnectionProperties.Create(fServer, fDatabase,
fUser, fPassword);
Data := fProps.Execute(fStatement, []);
DataResult := Data.FetchAllAsJSON(True, nil, False, True);
fProps.Free;
Happens when I use other versions of the MSSQL Connection Properties too.
I think it might be your Base64 converted to hex?
BinToHex(Data.ColumnBlob(0));
returns the same data
'44074100630063006F0075006E00740044114F007000650072006100740069006F006E0061006C00530074006100740075007300440A53007500620043006C007500730074006500720001014101024103470681020103410310E40400000200415381038101'
As for fConnectionString, I just stepped in the debugger, it is an empty string. So it is the same as fConnectionString := 'DataCompatibility=80;';
The full connection string will end up being 'Provider=SQLNCLI11;Data Source=.\sqlexpress;Initial Catalog=dummy;User Id=;Password=;Integrated Security=SSPI;Persist Security Info=False;'
Hi Arnaud,
Sorry for the late reply. There are no email notifications to the OP when a post is replied to
1. In any case, just to confirm whether my code is correct, I directly modified TOleDBMSSQL2012ConnectionProperties just to test things out:
procedure TOleDBMSSQL2012ConnectionProperties.SetInternalProperties;
begin
if OSVersion>wVista then
fProviderName := 'SQLNCLI11';
fConnectionString := fConnectionString + 'DataTypeCompatibility=80;';
inherited SetInternalProperties;
end;
This did not work. The column is still treated as an ftBlob. Tried the same with the 2008 driver. Client is using MSSQL 2008 by the way.
2. As for your second suggestion, I would like to avoid OpenQuery if possible (I'm worried about the part about data being passed twice... my resultset can sometimes be HUGE).
3. And here's a dump of what I got when I force the column type to ftUTF8 instead of ftBlob,
[{"XML_F52E2B61-18A1-11d1-B105-00805F49916B":"44074100630063006F0075006E00740044114F007000650072006100740069006F006E0061006C00530074006100740075007300440A53007500620043006C007500730074006500720001014101024103470681020103410310E40400000200415381038101"}]
while this is what I am supposed to get
<Account>
<OperationalStatus>6</OperationalStatus>
<SubCluster>AS</SubCluster>
</Account>
Thanks again!
Hi Arnaud,
As per my previous post, if I 'force' it as a column via sub-SELECT, then yes, it returns correctly as a string column using ColumnString or ColumnUTF8.
Otherwise, if I force the ColumnType to ftUTF8 in BindColumns, it will return the hex equivalent of the binary blob. The binary blob is not a real UTF-16, I suppose? Because if I force a conversion to UnicodeString using RawUnicodeToString(Data.ColumnBlob(0)), I get a mix of weird Asian characters especially where the angle brackets ("<" and ">") are.
Of course, if I do this
SELECT
(SELECT
AccountCode,
CompanyName,
(
SELECT Address1, Address2, PostCode
FROM vwAddress vad
WHERE vad.AccountID = va.AccountID
FOR XML PATH ('Address'), ROOT('Addresses'), TYPE
)
FROM VwAccount va
WHERE ClientID = @ClientID
FOR XML PATH ('Account')
)
then it works beautifully. But alas, I am not allowed to amend the code for their stored proc
Same result for both compatibility settings, and for both SQLNCLI11 and SQLNCLI10 drivers
I have a feeling I have to turn down this project...? Lol
It contains 128, which is DBTYPE_BYTES ... I guess that's why it is treated as BLOB?
Hi Arnaud,
Yes, I did try with the SQLNCLI11 provider. Also tried both of the DataTypeCompatibility values, neither work.
With ODBC, the statement executed, but .Step returns False?
In short, nope
It was already a miracle that the client agreed to let us take a peek at the source code for their stored proc :-/
Thanks for the reply!
Ord(Data.ColumnType(0)) returns 7, which maps to ftBlob, I believe.
There is no RawUTF8ToString function, I tried UTF8ToString, it output some binary characters.
RawUnicodeToString works partially, but 20% of the text got converted into some weird Asian characters
Hi Arnaud,
I have a stored procedure which returns an XML fragment due to the need to encapsulate a 1-to-Many result as one of the fields:
SELECT
AccountCode,
CompanyName,
(
SELECT Address1, Address2, PostCode
FROM vwAddress vad
WHERE vad.AccountID = va.AccountID
FOR XML PATH ('Address'), ROOT('Addresses'), TYPE
)
FROM VwAccount va
WHERE ClientID = @ClientID
FOR XML PATH ('Account')
And will generate something like
<Account><AccountCode>621</AccountCode><CompanyName>Acme Inc</CompanyName><Addresses><Address><Address1>21 Jump St</Address1><Address2 /><PostCode>214012</PostCode></Address></Addresses></Account>
Here is the code I use to test the execution of the Stored Proc:
var
Props: TSQLDBConnectionProperties;
Data: ISQLDBRows;
XML: RawUTF8;
begin
Props := TOleDBMSSQLConnectionProperties.Create(edtServer.Text, edtDatabase.Text, edtUsername.Text, edtPassword.Text);
Data := Props.Execute('Exec GetAccount ?', ['2928-12-34']);
while Data.Step() do
begin
XML := Data.ColumnString(0);
// Base64MagicDecode(XML); <--- this does not work either,
Memo1.Lines.Add(XML);
end;
Props.Free;
end;
I tried using Base64MagicDecode because the dump starts with
ï¿°RAdBAGMAYwBvAHUAbgB0AEQLQQBjAGMAbwB1AG
but it returns an empty string.
Did I miss something here?
Thank you!