#3 mORMot 1 » TSQLPropInfo.IsValueVoid for value of 0.1 returns true » 2022-07-19 11:51:46

Milos
Replies: 4

It seems that in Mormot 1.8 TSQLPropInfo.IsValueVoid returns true for 0.xxx values e.g. 0.1 and probably -0.1 as well, it relies on GetInt64 to test whether string represents a zero and that function does its job and returns zero for anything between -1 and +1.

#4 Re: mORMot 1 » Blank string fields cause strange things? » 2022-07-16 21:29:22

Hey no problem, it is a weird one I agree. And it doesn't happen with every empty string (in example above it happens with 2nd empty string) so perhaps add "sometimes" to the "ZDBC returns P<>nil " comment in code.

I am still on Mormot 1.8, I can paste the same code change there and give it a try tomorrow.

I wrote a post on Zeos forum but it seems to be still pending Admin approval... I think they should make sure to return nil/empty string from their code.

#5 Re: mORMot 1 » Blank string fields cause strange things? » 2022-07-16 18:04:28

Ah well, it just needed a little bit more banging my head against the keyboard and I found the problem.

In SynDBZeos.pas

procedure TSQLDBZEOSStatement.ColumnsToJSON(WR: TJSONWriter);

for fields of UTF8 type, the function calls

          P := fResultSet.GetPAnsiChar(col+FirstDbcIndex,Len);
          WR.AddJSONEscape(P,Len);

and in my example case, for one of the blank strings, Zeos returned zero length but a non-null pointer for some reason. Mormot ignores the length and accepts the PAnsiChar as-is which is IMO correct and the fault is on Zeos side. I guess they could say "check the length" but returning a dirty pointer is not nice regardless.

btw they have another GetPAnsiChar function, without the length parameter, and that function actually fixes the problem and returns an empty string which probably explains why the Zeos example works, it probably uses that one. IDK if it makes sense for Mormot to use that one, you do seem to need the length for jsonwriter.

btw #2 perhaps it is a bit strange for TTextWriter.AddJSONEscape to write anything if length is zero? edit: actually you want to write the empty string "" which makes sense, but then the code "finds data" since pointer turns out to have some data and data actually gets written, so maybe just kind of unexpected behaviour...

#6 Re: mORMot 1 » Blank string fields cause strange things? » 2022-07-15 20:37:23

I am back on this Firebird project and this problem is still there for me... can someone please tell me how/what to investigate further? I cant believe a problem like this would go unnoticed by others which implies I did something wrong, but I dont see what could I have done wrong?

I dug down as low as TZInterbase6XSQLDAResultSet.GetPAnsiChar which returns bad data as well. But when called from the ZSimple zeos example project, the data is correct which means something "in between" is different and it is really hard for me to figure out what and why sad

if the example is confusing, a simpler example is that I have 4 records in DB, with two string fields and if I sprinkle some empty strings in the DB data, the result becomes incorrect.

 
actual data:

ID Name1 Name2
1  'A'     '1'
2  'B'     ''
3  ''     '3'
4  'D'     '4'

mormot (or zeos) returns:
{"ID":1,"Name1":"A","Name2":"1"}
{"ID":2,"Name1":"B","Name2":""}
{"ID":3,"Name1":"A","Name2":"3"} --- Name1 should be blank!
{"ID":4,"Name1":"D","Name2":"4"}

Also, the first time I encountered the problem, same code seemed to later work fine on Postgre so it seems a Firebird-only problem.

#7 Re: mORMot 1 » A retry happens and I don't know why » 2022-05-17 20:00:54

No, no, I used sleep only to figure out whats going on - to simulate a timeout. I don't do it in real application. In practice I did not encounter timeouts except when debugging with delphi and pausing execution which does then cause confusion when a retry fires, that is how I first noticed it.

#8 Re: mORMot 1 » A retry happens and I don't know why » 2022-05-17 17:35:19

hmmm but even if it stopped at SockReceivePending it would still do a retry if I am not mistaken.

Thank you for the explanation on what is going on, but, what if we consider a scenario where client scans barcodes and adds products to the basket. In case of a timeout-induced retry, it would add two items to the basket instead of one. I suppose I could include some sort of non-repeatable client request ID so that the server knows that if an ID is repeated something has gone wrong, but then that would have to be part of every similar db-modifying operation if I'd want to be safe. Seems to me it would be easier for client side to handle occasional rare timeout errors (in the case above it would just mean "refresh the basket to see whats really in there before proceding") than to worry whether a successful request was actually executed twice.

#9 Re: mORMot 1 » A retry happens and I don't know why » 2022-05-17 16:06:48

I am using version 1.18, sorry, I should have mentioned and the code in question is in
function THttpClientSocket.Request
line 5737 unit SynCrtSock

#10 Re: mORMot 1 » A retry happens and I don't know why » 2022-05-17 15:47:51

after some digging the retry seems triggered from within THttpClientSocket.Request.

It sends the request, time passes, SockRecvLn comes back as empty string, it then calls DoRetry(STATUS_HTTPVERSIONNONSUPPORTED,Command)

Isn't this often unwanted, what if the call is making some changes like adding a record to the database? If for whatever reason server is slowed down it would add two records and the client program would not even know that a retry happened?

#11 mORMot 1 » A retry happens and I don't know why » 2022-05-17 15:31:50

Milos
Replies: 8

Short version: it seems as if the client always retries the request if it times out, even though Client.RetryOnceOnTimeout is set to false.

Long version:

I made a simple interface service method, which just sleeps for N seconds, for testing:

procedure TBaseAccess.Delay(const Seconds: integer);
begin
  WriteLn ('Delay has been called');
  sleep (Seconds * 1000);
end;

I call it from the client

  FClient.Service<IBaseAccess>.Delay(Seconds);

Client.RetryOnceOnTimeout is set to false and the receive timeout is shorter (3 seconds) than the amount of time specified for the delay (30 seconds)

The Delay method gets called twice and I don't understand why or how to avoid that.

Server log:

20220517 15161109  -    00.194.606
20220517 15161731  +    funServerMain.TfunRestServer(032b3080).URI POST FUNDUS/BaseAccess.Delay?session_signature=33cd9752001b21f784cd2762 in=4 B
20220517 15161731 call          funServerMain.TfunRestServer(032b3080) IBaseAccess.Delay[30]
Delay has been called
20220517 15161827 srvr          funServerMain.TfunRestServer(032b3080) IIIb  POST FUNDUS/BaseAccess.Delay Interface=200 out=13 B in 30s
20220517 15161827 ret           funServerMain.TfunRestServer(032b3080) {"result":[]}
20220517 15161827  -    30.012.093
20220517 15162131  +    funServerMain.TfunRestServer(032b3080).URI POST FUNDUS/BaseAccess.Delay?session_signature=33cd9752001b21f784cd2762 in=4 B
20220517 15162131 call          funServerMain.TfunRestServer(032b3080) IBaseAccess.Delay[30]
Delay has been called
20220517 15162227 srvr          funServerMain.TfunRestServer(032b3080) IIIb  POST FUNDUS/BaseAccess.Delay Interface=200 out=13 B in 30s
20220517 15162227 ret           funServerMain.TfunRestServer(032b3080) {"result":[]}
20220517 15162227  -    30.012.076
20220517 15164731 srvr          funServerMain.TfunRestServer(032b3080) IIIb  POST FUNDUS/BaseAccess.Delay Interface=200 out=13 B in 30s
20220517 15164731 ret           funServerMain.TfunRestServer(032b3080) {"result":[]}
20220517 15164731  -    30.004.667

Help pls?

#12 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-04-10 17:30:54

Maybe best to ignore me as I think I am going insane because now I am re-testing and the approach I wrote in post #7 seems to be working, except in one case where trying to start the transaction crashes with message "database should be connected" and I don't see whats different about that case... but I will try to figure it out.

#13 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-04-09 12:58:18

  mu := TSqlMeasurementUnit.Create;
  mu.Name := 'TST';
  mu.Symbol := 'TST';
  RestServer.TransactionBegin(TSqlMeasurementUnit);
  RestServer.Add(mu);
  RestServer.Rollback;

if I execute this on server the transaction seems to have no effect, if I stop after the Add() line the record is commited to the (postgre) database. Rollback doesn't seem to have any effect.

Could it be I have misconfigured something?
(edit: I checked and TransactionBegin returns true. Also, I tried supplying SessionID=0 to the transaction calls, as suggested earlier in this thread, but there is no change in outcome)

#14 Re: mORMot 1 » How to get the actual exception message if it occurs within a batch? » 2022-04-07 20:20:06

I know it would not be atomic, it was meant to help me either ensure that the values are ok to put in the batch before I run the batch, or to use when batch fails in order to find which field caused the problem, as a diagnostic basically. But since batch doesn't help me I will have to rethink the whole thing.

Oops, Thank you for the info on Length<>Count, I was not aware and will alter my code smile

#15 Re: mORMot 1 » How to get the actual exception message if it occurs within a batch? » 2022-04-07 12:55:51

Ah, but this scenario is about batch processing, so I can't use Add().

And I used batch only because I assumed it was a replacement for multi-table atomic DB transaction, but thats a separate issue in a separate topic.

#16 Re: mORMot 1 » How to get the actual exception message if it occurs within a batch? » 2022-04-07 11:21:50

function TfunRestServer.CheckUniqueFields(rec: TSqlRecord): string;
var
  ID: TID;
  value: RawUTF8;
begin
  for var field in rec.RecordProps.Fields.List do
    if aIsUnique in field.Attributes then
      begin
        value := field.GetValue(rec, true);
        if field.SQLFieldType in STRING_FIELDS then value := '''' + value + '''';

        ID := OneFieldValueInt64(rec.RecordClass, 'rowID',
                                 FormatUTF8 ('(% = %)', [field.Name, value], []), 0);
        if (ID <> 0) and (ID <> rec.IDValue)
           then exit (field.Name);
      end;
  result := '';
end;

This seems to do the trick, though perhaps I missed an easier or more elegant way, in particular am not sure if there is a better way to convert field value to sql.

(code is part of a class that inherited TSQLRestServerDB)

#17 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-04-07 10:31:59

Welp... I have just realised that batches have separate transactions for each table involved.

So, if anything unexpected were to happen such as hardware failure at the wrong moment, the batch does not provide integrity safety for the entire operation.

Is there any way for me to add something like that? Why do the transactions have to be per-table and not for the entire database?

#18 Re: mORMot 1 » How to get the actual exception message if it occurs within a batch? » 2022-04-07 10:14:34

It is not the primary key field, it is one of the "normal" fields marked as unique which caused this exception.

I think maybe I will add some generic code to "crawl" through all unique fields and check if their values are already present in the table before doing insert/update so that I can notify the user in which field he entered a duplicate value.

A modern neat alternative is to have this type of check for every unique field as soon as user types it, the way many website registrations work when you enter a username and it immediately adds a warning if it is already in use, but that requires way more code of course and there is always the chance that after doing the check some other user will enter that same value and the check becomes irrelevant and the operations fails after all.

#19 mORMot 1 » How to get the actual exception message if it occurs within a batch? » 2022-04-06 18:55:50

Milos
Replies: 7

Unless if I am missing something, if an sql error happens during a batch operation there is no way to report that error back to the client because the batch masks the exception and then raises its own exception which does not contain the sql error?

Log shows both exceptions

20220406 18503808 EXC                   EZSQLException ("SQL Error: ERROR:  duplicate key value violates unique constraint \"article_ident_key\"\nDETAIL:  Key (ident)=(11111) already exists.") [HttpSvr 8889/FUNDUS THttpApiSvr] at b0df7a
20220406 18532930 EXC                   EORMBatchException {"Message":"TfunRestServer.EngineBatchSend: Results[0]=0 on POST@Article TSqlArticle"} [HttpSvr 8889/FUNDUS THttpApiSvr] at 75fb84

but if I wrap batch execution in my try except I will of course only be aware of the second (final) one. Any way around it?

#20 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-04-04 08:50:09

Thank you, that generator can help me solve the problem by using batch instead of transactions, but if that is the path to take I think I will instead read the max ID for the table at the server's start and use my own generator to simply autoincrement (within a critical section ofc) instead.

@pvn0 I don't need a pivot table, it is a simple matter of one master record in one table and multiple many-many records in another table, all of which I would like to insert in one, "atomic", operation.

#21 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-04-03 13:49:35

Sorry for - again - ressurecting this topic but I am still confused and my code (which I thought I tested and seemed fine) doesn't work. The moment I call ORM .Add the record is in the database even if transaction is still not commited.

Can we please start over with an example? Here's a function, on the server, exposed via service interface

function TUserAdministration.Add (var User: TSqlFunUser; const WarehouseIDs: TIDDynArray): TOutcome;

The goal is to create a new user and immediately link it with N warehouses (many-many) and if any part of the process fails I don't want any trace of it in the database. In the past I always used transactions for such things but I think it is either not the Mormot way or I don't know how.

I would use batch instead of transaction but I (seemingly) can't do that because I can't know the ID of the newly created FunUser record to supply as SourceID for the subsequent many-many inserts. Is there a way to "reserve" an unique ID in advance and use it for such a batch operation?

#23 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-02-28 15:36:23

Thank you for the responses, I am still unsure what is best to do if the start or rollback crash though, I suppose if the called code is written in a safe way then a crash in Start should mean there is no transaction but if Rollback crashes then I am not sure what the state is...

#24 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-02-28 12:49:25

OK I agree that commit can legitimately raise an exception depending on what the database operations were, in which case I should catch the exception and do a rollback instead of halting.

But if an exception happens for transaction start or rollback that seems like something that should never happen? I am afraid of leaving the database is in some unexpected state such as having an open transaction that would never be commited so any later updates would "go into the wind".

#25 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2022-02-27 14:08:26

My goal is to use this as a foolproof simple way of using transactions both for ORM and "direct" queries via DB.Execute.

I revised my code but would GREATLY appreciate if someone could ease my mind about this being correct, or point out if there is a problem, or tell me if I reinvented the wheel and should have used some already existing functionality.

  TfunRestServer = class (TSQLRestServerDB) 
// class extended for easier transactions
    DBConnectionProperties: TSQLDBConnectionPropertiesThreadSafe;
    procedure LockAndStartTransaction;
    procedure CommitAndUnlock;
    procedure RollbackAndUnlock;
procedure TfunRestServer.LockAndStartTransaction;
begin
  try
    fAcquireExecution[execORMWrite].Safe.Lock;
    DBConnectionProperties.MainConnection.StartTransaction;
  except
    halt; // I am paranoid about transaction problems leading to undetermined DB state at the moment...
  end;
end;

procedure TfunRestServer.CommitAndUnlock;
begin
  try
    DBConnectionProperties.MainConnection.Commit;
    fAcquireExecution[execORMWrite].Safe.UnLock;
  except
    halt; // I am paranoid about transaction problems leading to undetermined DB state at the moment...
  end;
end;

procedure TfunRestServer.RollbackAndUnlock;
begin
  try
    DBConnectionProperties.MainConnection.Rollback;
    fAcquireExecution[execORMWrite].Safe.UnLock;
  except
    halt; // I am paranoid about transaction problems leading to undetermined DB state at the moment...
  end;
end;

#26 Re: mORMot 1 » Stop the war! » 2022-02-27 13:24:02

It is truly terrible what is happening and I hope the madness will stop soon. Try to stay safe

#27 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2021-10-12 11:44:16

Thank you for the response, I have a rest server with interface based services so I will definitely take a look at the global lock then!

#28 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2021-10-11 23:12:08

This seems to work, but is it the correct/safe way to do it?

success := false;
DB.MainConnection.StartTransaction;
try
  success := do something with db and do something else etc...
  if success then DB.MainConnection.Commit;
finally
  if not Success
    then DB.MainConnection.RollBack;
end;

#29 Re: mORMot 1 » How to correctly call TRestServerDB.TransactionBegin? » 2021-10-11 18:48:28

I have the need for server-side transactions as well and am feeling lost... so, I will use this thread to ask instead of opening my own, I hope that is ok.

Batches seem to be the suggested method instead of transactions, but what about adding a record and a number of TSQLRecordMany aggregate records in one transaction, batch can't help with that unless if I am missing something? I need the ID of the new record which I cant have since the insert is part of batch? I suppose I should ask mormot for a fresh record ID (how?) and use the ForceID=true when adding the record?

btw I tried with transactions but I must have setup something incorrectly because they dont seem to have an effect, the moment I add a record for example I can see it in PGAdmin...

#30 mORMot 1 » Blank string fields cause strange things? » 2021-08-29 11:18:43

Milos
Replies: 6

In Firebird 2.5 I have a simple table with 2 string fields and if some of the values in the database are blank, retrieving the data seems wrong as low as FetchAllToJSON level.

In this example (all code on server side), I create 4 records, first one has both string fields assigned, second has first field empty but second is assigned, third is reverse situation, fourth has both fields assigned.

However, the resulting data has one of the field values repeated where an empty string should be.

  TSqlMeasurementUnit = class (TSQLRecord)
  private
    FName: RawUTF8;
    FSymbol: RawUTF8;
  public
  published
    property Name: RawUTF8 index 20 read FName write FName;
    property Symbol: RawUTF8 index 5 read FSymbol write FSymbol;
  end;
    RestServer.Execute('delete from MeasurementUnit');

    var mu : TSqlMeasurementUnit;

    mu := TSqlMeasurementUnit.Create;

    mu.Name := '+N1+'; // add a normal one
    mu.Symbol := '+S1+';
    RestServer.Add (mu, true);

    mu.Name := ''; // add one with blank name
    mu.Symbol := '-NN-';
    RestServer.Add (mu, true);

    mu.Name := '-NS-';
    mu.Symbol := ''; // add one with blank symbol
    RestServer.Add (mu, true);

    mu.Name := '+N2+';  // add another normal one
    mu.Symbol := '+S2+';
    RestServer.Add (mu, true);
    mu.Free;

    var ol := RestServer.RetrieveList<TSqlMeasurementUnit>;
    for mu in ol do
      writeln (ObjectToJSONDebug(mu));

The database viewed from db workbench shows

ID              NAME                                                                             SYMBOL              
---------------------------------------------------------------------------------------------------------------------
1               +N1+                                                                             +S1+                
2                                                                                                -NN-                
3               -NS-                                                                                                 
4               +N2+                                                                             +S2+                

The server log shows 3rd record symbol field having +S1+ value (value from the first record). I traced as far as FetchAllToJSON and it returns

'[{"ID":1,"NAME":"+N1+","SYMBOL":"+S1+"},{"ID":2,"NAME":"","SYMBOL":"-NN-"},{"ID":3,"NAME":"-NS-","SYMBOL":"+S1+"},{"ID":4,"NAME":"+N2+","SYMBOL":"+S2+"}]'#$A

I will try to dig deeper a bit later.

After more digging I guess I enter Zeos territory... I updated to latest but the problem is still there...

edit#3 Tried the Zeos ZSimple example and the same query seems to return correct results...

#31 Re: mORMot 1 » Row-level (multitenant) access control » 2021-01-09 15:14:05

OK, thanks for the answer, I will think about it some more

#32 mORMot 1 » Row-level (multitenant) access control » 2021-01-08 20:32:11

Milos
Replies: 2

Sorry if this was already asked, I only found one semi-related topic that doesn't really match the question.

From what I understand, if I use interfaces or server methods to return data I can ensure row-filtering by utilizing the user's tenant ID which I can get by looking for the AUTH record with ID = ServiceContext.Request.SessionUser - hopefully this is the valid approach. Is there a way to do the same for plain ORM calls? I see there is a BeforeURI event but I am not sure it provides all the info, and even if it does I am not sure how to force/inject the parameter value into subsequent ORM? Or maybe there is some other way?

For example, let's say employees work in different departments, they have access to the Documents table but each should only see the documents of his department. Is there a way to ensure this on server side without relying on clients being "honest" and supplying their tenant ID as part of the query filter? Basically look up user's tenant (department) ID and force it into the where clause or something like that?

#34 Re: mORMot 1 » Struggling to get an array of objects from interface service » 2021-01-05 16:31:33

Thanks, I have just recently started to more seriously examine Mormot so I am not really familiar with version 2 and whether I should focus on that one instead, I think I will do some more testing and research with 1.18 but maybe re-think when it comes time to start with "heavy" development.

#35 Re: mORMot 1 » Struggling to get an array of objects from interface service » 2021-01-04 18:36:26

AAH! It makes sense of course but my brain didn't see it...

Thanks! Merci beaucoup!

#36 mORMot 1 » Struggling to get an array of objects from interface service » 2021-01-04 18:18:01

Milos
Replies: 6

Hello, I searched a lot, found some answers but they didn't work for me, I must be doing something wrong and not seeing it.

I would like the interface to return an array of TSQLPerson objects. Instead, I seem to get an array of integers.

  TSQLPerson = class(TSQLRecord)
  private
    fName: RawUTF8;
    fBirthDate: TDateTime;
  public
  published
    property Name: RawUTF8 index 50 read fName write fName;
    property BirthDate: TDateTime read fBirthDate write fBirthDate;
  end;

  TSQLPersonObjArray = array of TSQLPerson;

  ITest = interface(IInvokable)
    ['{3B4FEE39-C650-44C7-A6AF-0C9121342BBD}']
    procedure OnePerson (out Person: TSQLPerson);
    procedure Persons   (out PersonObjArray: TSQLPersonObjArray);
  end;

...

  TInterfaceFactory.RegisterInterfaces([TypeInfo(ITest), TypeInfo(ITest)]);
  TJSONSerializer.RegisterClassForJSON([TSQLPerson]);
  TJSONSerializer.RegisterObjArrayForJSON(TypeInfo(TSQLPersonObjArray), TSQLPerson);

...

procedure TTest.Persons(out PersonObjArray: TSQLPersonObjArray);
var
  Person : TSQLPerson;
begin
  Person := TSQLPerson.Create;
  Person.Name := 'John Doe';
  Person.BirthDate := now - 365*30;
  ObjArrayAdd(PersonObjArray, Person);
  Person := TSQLPerson.Create;
  Person.Name := 'Jane Doe';
  Person.BirthDate := now - 365*40;
  ObjArrayAdd(PersonObjArray, Person);
end;

OnePerson method works while the Persons method seems to return an array of integers.
Server debug output:

 {"result":[[54783928,54783952]]}

I tried with TSynAutoCreateFields descendants instead of TSQLRecord but the outcome was the same.

What am I doing wrong?

Board footer

Powered by FluxBB