I am building a SQL query builder for a repository. This builder will find all required fields for a query and create the SQL text. For this, I am using the Format() procedure. But, I am having trouble creating in runtime the TVarRec array that I must pass to the Format procedure.
It's easy to build this array using constants like Format('%s, %s', ['AString', 'AnotherString']);`. But how must we do to create it in runtime ?
Here is a simplified version of my approach :
procedure BuildString;
begin
FStrings := TStringList.Create;
FStrings.Add('String 1');
FStrings.Add('String 2');
FStrings.Add('String 3');
FFormatString := '%0:s, %1:s, %2:s';
SetLength(FFormatStringParams, FStrings.Count);
for I := 0 to FStrings.Count - 1 do
begin
aString := FStrings.Strings[I];
FFormatStringParams[I].VString := Addr(aString);
end;
ShowMessage(Format(FFormatString, FFormatStringParams));
end;
But when I run this, I get the error "The format '%0:s, %1:s, %2:s' is invalid or incompatible with the argument'
I understand that I am incorrectly building the TVarRec array that I must pass to the Format procedure. Can anyone help me in this?
Thank you.
You are passing the address of the string variable rather than its value. Furthermore, you are assigning the VString field, which expects a ShortString. You are supplying a native String instead.
Replace
VString := Addr(aString)
with
VUnicodeString := Pointer(aString)
Furthermore, you need to specify the string type by assigning the VType field.
for I := 0 to FStrings.Count - 1 do
begin
aString := FStrings.Strings[I];
FFormatStringParams[I].VType := vtUnicodeString;
FFormatStringParams[I].VUnicodeString := Pointer(aString);
end;
Related
I have a StringList with 2 rows
aOldPriceTerms[0] = 'FuelAddition=336643160'
aOldPriceTerms[1] = 'Freight=336643155'
So it works fine to use
aOldPriceTerms.Values['Freight'] -> '336643155'
I want to have a list of ID's from the list.
So simply
'FuelAddition','Freight'
Currently I use this code where aOldPriceTerms is the actual StringList.
function GetList(aOldPriceTerms: TStringList): String;
var
vOldTerm : string;
vOldTermsList : TStringList;
begin
vOldTermsList := TStringList.Create;
try
for i := aOldPriceTerms.Count - 1 downto 0 do
begin
vOldTerm := Copy(aOldPriceTerms[i], 1, (Pos('=', aOldPriceTerms[i])-1));
vOldTermsList.Add(vOldTerm);
end;
Result := vOldTermsList.CommaText;
finally
FreeAndNil(vOldTermsList);
end;
end;
My question is there a cleaner way to get the ids ?
Example is from the Delphi Basics, but TStringList.Names is also described in the Delphi documentation
// Now display all name and age pair values
for i := 0 to names.Count-1 do
begin
ShowMessage(names.Names[i]+' is '+names.ValueFromIndex[i]);
end;
You can use the TALNVStringList (NV for nameValue) from alcinoe (https://github.com/Zeus64/alcinoe) to handle Name and Value without all the time splitting the string
TALNVStringList is exactly the same as TStringList (nothing to change in the code except replacing TstringList by TALNVStringList) except that it's more efficient in speed because it's store the name in one dedicated field and the value in another dedicated field (no need to do all the time pos('=') and copy() to retrieve the name and the value of the row)
for i := 0 to aOldPriceTerms.Count-1 do
begin
ShowMessage(aOldPriceTerms.Names[i]+' is '+aOldPriceTerms.ValueFromIndex[i]);
end;
Exe demo showing the speed penalty of classic TstringList: https://svn.code.sf.net/p/alcinoe/code/demos/ALSortedListBenchmark/win32/ALSortedListBenchmark.exe
To get all the id's
for name in names.Names do
begin
i := names.IndexOf[name];
end;
or
To get all the Values
for name in vOldTermsList.Names do
begin
Value := vOldTermsList.Value[name];
end;
I'm trying to develop a simple app based on examples:
http://docwiki.embarcadero.com/RADStudio/XE3/en/Creating_the_Server_Side_with_DataSnap_Server_(InterBase_Tutorial)
http://docwiki.embarcadero.com/RADStudio/XE3/en/Creating_a_Rich-Client_Interface_(InterBase_Tutorial)
I'm trying to implement a generic procedure to call DB stored procedures. The parameters in this procedure will be passed as a Variant array
So the question is:
How to pass Variant parameter to the procedure of the DataSnap server module?
Here so, for example, doesn't work, because TObject it not Variant; something like "SetVariant" i didn't find:
function TdmServerModuleClient.callStoredProcedure(aSPName: String;
aParamNames, aParamValues: Variant): Integer;
begin
if FcallStoredProcedureCommand = nil then
begin
FcallStoredProcedureCommand := FDBXConnection.CreateCommand;
FcallStoredProcedureCommand.CommandType := TDBXCommandTypes.DSServerMethod;
FcallStoredProcedureCommand.Text := 'TdmServerModule.callStoredProcedure';
FcallStoredProcedureCommand.Prepare;
end;
FcallStoredProcedureCommand.Parameters[0].Value.SetString(aSPName);
FcallStoredProcedureCommand.Parameters[1].Value.SetObjectValue(aParamNames);
FcallStoredProcedureCommand.Parameters[2].Value.SetObjectValue(aParamValues);
FcallStoredProcedureCommand.ExecuteUpdate;
Result := FcallStoredProcedureCommand.Parameters[1].Value.GetInt32;
end;
I'm trying to do the following: convert multiple values into array of Variant. I use this code to do this:
function ValuesToArray(aValues: array of Variant): Variant;
var
i: Integer;
begin
Result := VarArrayCreate([Low(aValues), High(aValues)], varVariant);
for I := Low(aValues) to High(aValues) do
Result[i] := aValues[i];
end;
After, I'm trying to pass this array (of type Variant) in the procedure, which is in my server module via client class TdmServerModuleClient, described above.
After, I'm going to convert the variant array into values of different types on the server side. But the TDBXCommand does not allow me to set the parameter value to be passed as Variant.
As for the phrase "you may just serialize those as text", could you explain (with code if possible) how I can deserialize such text in my server module?
With an TADOQuery.Locate that uses a list of fields and a VarArray of values, if one of the values contains a # sign, we get this exception:
'Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another.'
I've traced this down to ADODB which itself seems to be using # signs as delimiters.
Is there a way to escape #-signs so that the query doesn't fail?
* EDIT 1 *
I was wrong. What causes this failure is a string that has a pound sign and a single quote. The code shown below fails with error message noted above.
What really worries us is that when it fails running as an .exe outside the IDE, there's no runtime exception. We only see the exception when we're in the IDE. If our programmers hadn't happened to be using data that triggers this we never would have known that the .Locate returned FALSE because of a runtime error, not because a matching record was not found.
Code:
var
SearchArray: Variant;
begin
SearchArray := VarArrayCreate([0,1], VarVariant);
SearchArray[0] := 'T#more''wo';
SearchArray[1] := 'One';
ADOQuery.Locate('FieldName1;FieldName2', SearchArray, []);
Please see Updates below; I've found a work-around that's at least worth testing.
Even with Sql Server tables, the # shouldn't need to be escaped.
The following code works correctly in D7..XE8
procedure TForm1.Button1Click(Sender: TObject);
begin
AdoQuery1.Locate('country;class', VarArrayOf(['GB', Edit1.Text]), []);
end;
when Edit1.Text contains 'D#E', so I think your problem must lie elsewhere. Try a minimalist project with just that code, after rebooting your machine.
Update: As noted in a comment, there is a problem with .Locate where the expression
passed to GetFilterStr (in ADODB.Pas) contains a # followed by a single quote. To try and
work out a work-around for this, I've transplanted GetFilterStr into my code and have
been experimenting with using it to construct a recordset filter on my AdoQuery, as I noticed
that this is what .Locate does in the statement
FLookupCursor.Filter := LocateFilter;
The code I'm using for this, including my "corrected" version of GetFilterStr, is below.
What I haven't managed to figure out yet is how to avoid getting an exception on
AdoQuery1.Recordset.Filter := S;
when the filter expression yields no records.
(Btw, for convenience, I'm doing this in D7, but using XE8's GetFilterStr, which is why I've had to comment out the reference to ftFixedWideChar)
function GetFilterStr(Field: TField; Value: Variant; Partial: Boolean = False): WideString;
// From XE8 Data.Win.ADODB
var
Operator,
FieldName,
QuoteCh: WideString;
begin
QuoteCh := '';
Operator := '=';
FieldName := Field.FieldName;
if Pos(' ', FieldName) > 0 then
FieldName := WideFormat('[%s]', [FieldName]);
if VarIsNull(Value) or VarIsClear(Value) then
Value := 'Null'
else
case Field.DataType of
ftDate, ftTime, ftDateTime:
QuoteCh := '#';
ftString, ftFixedChar, ftWideString://, ftFixedWideChar:
begin
if Partial and (Value <> '') then
begin
Value := Value + '*';
Operator := ' like '; { Do not localize }
end;
{.$define UseOriginal}
{$ifdef UseOriginal}
if Pos('''', Value) > 0 then
QuoteCh := '#' else
QuoteCh := '''';
{$else}
QuoteCh := '''';
if Pos('''', Value) > 0 then begin
QuoteCh := '';
Value := QuotedStr(Value);
end;
{$endif}
end;
end;
Result := WideFormat('(%s%s%s%s%2:s)', [FieldName, Operator, QuoteCh, VarToWideStr(Value)]);
end;
procedure TForm1.CreateFilterExpr;
var
S : String;
begin
// clear any existing filter
AdoQuery1.Recordset.Filter := adFilterNone;
AdoQuery1.Refresh;
if edFilter.Text = '' then Exit;
S := GetFilterStr(AdoQuery1.FieldByName('Applicant'), edFilter.Text, cbPartialKey.Checked);
// Add the filter expr to Memo1 so we can inspect it
Memo1.Lines.Add(S);
try
AdoQuery1.Recordset.Filter := S;
AdoQuery1.Refresh;
except
end;
end;
procedure TForm1.FilterClick(Sender: TObject);
begin
CreateFilterExpr;
end;
Update 2: Try the following:
Copy Data.Win.ADODB.Pas to your project directory
In it, replace GetFilterExpr by the version above, making sure that UseOriginal
isn't DEFINEd, and that ftFixedWideChar is reinstated in the Case statement.
Build and run your project
In XE8 at any rate, my testbed now correctly Locate()s a field ending with ' or #'
(or containing either of them if loPartialKey is specified. (I can't test in XE4/5
because my XE4 now says it's unlicenced since I upgraded to Win10 last week, thanks EMBA!)
I hestitate to call this a solution or even a work-around as yet, but it is at least worth testing.
I'm not sure whether I'd call the original version of GetFilterExpr bugged, because I'm not sure
what use-case its treatment of values containing quotes was intended to handle.
I have an ADO Connection String:
Provider=SQLOLEDB.1;Data Source=MYCOMPUTER\SQL2008;User ID=GuestUser;Password=password;Persist Security Info=True;Initial Catalog=DefaultDatabase;
And I want to read specific parts of the string into various TEdit controls. Is there a function that is able to parse that information for me or am I going to need to split strings at the semicolons, then again by the equal signs and read the first index of that string array?
If you just want to parse the string as it is you can use a StringList setting Delimiter and StrictDelimiter and assign DelimitedText. You may then use Names and Values of the StringList.
var
i:Integer;
sl:TStringList;
begin
sl:=TStringList.Create;
try
sl.Delimiter :=';';
sl.StrictDelimiter := true;
sl.DelimitedText := Con.ConnectionString;
Memo1.Lines.Assign(sl);
finally
sl.Free;
end;
end;
Access e.g. via sl.Names[i]; sl.Values['Password']; sl.ValueFromIndex[i]
Ado itself allows access through Properties which will list more than the assigned values and would be the preferable way.
var
i:Integer;
begin
For i := 0 to Con.Properties.Count - 1 do
begin
Memo1.Lines.Add(Con.Properties[i].Name + '='+ VarToStr(Con.Properties[i].Value));
end;
end;
Is there a better way to create objects in IScriptControl than this?
Result := SC.Eval('new Date()');
I need something like this:
function CreateJSObject(JSClassName: string; Params: PDispParams): OleVariant;
a naive implementation would be
var
S: string;
begin
S := '';
for I := P.cArgs - 1 downto 0 do
begin
if S <> '' then
S := S + ', ';
S := S + ConvertParamToJSSyntax(OleVariant(P.rgvarg[I]));
end;
Result := ScriptControl.Eval('new ' + JSClassName + '(' + S + ');');
end;
Query the IDispachEx interface on the CodeObject property of the MSScriptControl. It is a pointer on the global state of the JScript and it contains all objects added to it. Then do an InvokeEx with a DISPATCH_CONSTRUCT parameter on the object name you want to create. This would be equivalent to calling "new".
This would create an object of the correct type and you would not have to convert them to javascript types. You'll be able to pass native objects to the constructor as well.
I know that this works for constructors defined in script. I'm not sure about Date which is a native property.
This works on JScript and VBScript activescripting host, but some others scripting host does not return anything on CodeObject, so this is not very portable.
To call a subroutine, you need to use the Run method, instead of Eval. See this doc for more info.
You are correct in saying that "constructors are different sort of methods", but in this case you are actually just returning the newly-constructed value, aren't you? And so I would expect to still be able to use Eval().
The following code works for me:
procedure TForm1.Button1Click(Sender: TObject);
var
ScriptControl: Variant;
Value: Variant;
begin
ScriptControl := CreateOleObject('ScriptControl');
ScriptControl.SitehWnd := Handle;
ScriptControl.Language := 'JScript';
Value := ScriptControl.Eval('new Date();');
ShowMessage(VarToStr(Value));
end;
When I click the button, my ShowMessage shows up with "Wed Sep 16 23:37:14 TC+0200 2009".
And so for returning a value from a constructor, you can actually use Eval().