Recent

Author Topic: JSON with SQLite. SELECT json(jfield) FROM MYTABLE gives string of 1021 chars  (Read 452 times)

strubri

  • Newbie
  • Posts: 6
I came across an interesting situation. Reporting it here to help anyone who may have stumbled on this problem.

Lets say we have a table in an SQLite database with a JSON field.
Code: SQL  [Select][+][-]
  1. CREATE TABLE MyTable (
  2.     id INTEGER PRIMARY KEY AUTOINCREMENT,
  3.     jsonObj JSON    
  4. )        
  5.  

We store values into field jsonObj with JSONB() so that it is stored in a binary format for better indexing/updating. The SQLite documentation for this is very clear as is the functionality.

Code: Pascal  [Select][+][-]
  1. procedure saveJSONObj(constref _jsonObj: TJSONObject);
  2. const
  3.  
  4.     Q =   'INSERT INTO MyTable(jsonObj) ' + sLinebreak
  5.         + 'VALUES (jsonb(:jsonObj))'; // note that the SQLite function jsonb() is called before storing the value in the field
  6.  
  7. var
  8.     _qry: TSQLQuery;
  9. begin
  10.     _qry := dbModule.newQuery(Q); // This is a helper function that instantiates a TSQLQuery object,
  11.                              // assigns the current open SQLite Connection and initializes the Transaction
  12.                              // It also sets the SQL property to the insert command in "Q"
  13.     try
  14.         _qry.Params[0].AsString := _jsonObj.FormatJSON(AsJSONFormat);
  15.         _qry.ExecSQL;
  16.     finally
  17.        _qry.Free;
  18.     end;
  19. end;
  20.  

It is pretty straight foward up to this point. However when we want to retrieve the JSON from the database, the following code fails because the Fields[] object only returns a string that is maximum 1021 characters  (in Lazarus 2.0.12, which i have to maintain an older code base)

Code: Pascal  [Select][+][-]
  1. // *** THIS FAILS ***
  2. function getJSONObj(_ID: int64): TJSONObject;
  3. const Q = 'SELECT json(jsonObj) FROM MyTable WHERE id = :id';
  4. var
  5.         _qry: TSQLQuery;
  6.  
  7.     function parseJSON(const _json: string): TJSONData;
  8.     begin
  9.         with TJSONParser.Create(_json, [joUTF8]) do // in unit jsonparser
  10.         begin
  11.             try
  12.                 try
  13.                     Result := Parse;
  14.                 except
  15.                     Result:= nil;
  16.                 end;
  17.             finally
  18.                Free; // the TJSONParser object
  19.             end;
  20.         end;
  21.     end;
  22.  
  23. begin
  24.     Result := nil;
  25.     _qry := dbModule.newQuery(Q); // This is a helper function that instantiates a TSQLQuery object,
  26.                                   // assigns the currenty open SQLite Connection and initializes the Transaction
  27.                                   // It also sets the SQL property to the insert command in "Q"
  28.     try
  29.         _qry.Params[0].AsLargeInt := _ID;
  30.         _qry.Open;
  31.         if not _qry.Fields[0].IsNull then begin
  32.             _jsonStr := _qry.Fields[0].AsString; // this contains only 1021 characters. JSON parsing fails for large objects
  33.              Result  := TJSONObject(parseJSON(_jsonStr));
  34.                 end;
  35.         finally
  36.         _qry.Free;
  37.         end;
  38. end;
  39.  
  40.  

At this point I do no know where the error lies. I tried many different things to get this to work including things like:
Code: SQL  [Select][+][-]
  1. SELECT CAST(json(jsonObj) AS TEXT) AS jtext FROM MyTable
  2.  

The value delivered by ... Fields[0].AsString is always truncated at 1021 characters.

Workaround
I have to store the original json value in a TEXT field in the table.
Code: SQL  [Select][+][-]
  1. CREATE TABLE MyTable (
  2.     id INTEGER PRIMARY KEY AUTOINCREMENT,
  3.     jsonObj JSON,
  4.     jsonObjText TEXT    
  5. )        
  6.  

Now when I retrieve the value with
Code: SQL  [Select][+][-]
  1. SELECT jsonObjText FROM MyTable
  2.  
the json object is parsed correctly.

I assume it is because TSQLQuery cannot correctly identify that the field returned by the SELECT statement is in fact a TEXT field (ftMemo). The value is shown correctly within DB Browser (for SQLite). This could be driver issue, it could be a Lazarus LCL issue.

Of course, this means that any updates to the table must happen on both fields. You could use the same parameter name in the prepared SQL statement so that the pascal code only needs to assign the valid JSON text to only one ParameterByName(). Something like this:

Code: Pascal  [Select][+][-]
  1. procedure saveJSONObj(const _id: int64; constref _jsonObj: TJSONObject);
  2. const
  3.     Q =
  4.       'UPDATE MyTable ' + sLinebreak
  5.     + 'SET jsonObj = jsonb(:jsonObj), ' + sLinebreak // note that the SQLite function jsonb() is called before storing the value in the field
  6.     + 'jsonObjText = :jsonObj' + sLinebreak // note that the same parameter name is assigned here. Internally this registers only once in the list of Params
  7.     + 'WHERE id = :id';
  8.  
  9. var
  10.     _qry: TSQLQuery;
  11. begin
  12.     _qry := dbModule.newQuery(Q); // This is a helper function that instantiates a TSQLQuery object,
  13.                                   // assigns the currenty open SQLite Connection and initializes the Transaction
  14.                                   // It also sets the SQL property to the insert command in "Q"
  15.     try
  16.         _qry.ParamByName('jsonObj').AsString := _jsonObj.FormatJSON(AsJSONFormat);
  17.         _qry.ParamByName('id').AsLargeInt    := _id;
  18.         _qry.ExecSQL;
  19.         finally
  20.         _qry.Free;
  21.         end;
  22.  
  23. end;
  24.  
  25.  



 
« Last Edit: September 05, 2024, 07:52:36 pm by strubri »

Zvoni

  • Hero Member
  • *****
  • Posts: 2692
There is no JSON-DataType (!!) in SQLite.
SQLite stores JSON always as TEXT
Code: SQL  [Select][+][-]
  1. CREATE TABLE MyTable (
  2.     id INTEGER PRIMARY KEY AUTOINCREMENT,
  3.     jsonObj JSON    /* <-- Nonsense */
  4. )  

SQLite only has JSON-Functions and operators

And don't use AUTOINCREMENT for your Primary Key. You don't need it.

EDIT: And since you INSERT/UPDATE a JSONB, use BLOB as storage-Class
« Last Edit: September 06, 2024, 12:01:22 pm by Zvoni »
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

strubri

  • Newbie
  • Posts: 6
JSON type was introduced in SQLite recently. Yes it used to that you could only store a JSON value as text before. I don't know how it stores it internally now but they have updated the driver to allow devs to work better with JSON values. I tested this on SQLite  Version 3.46.0

I find that the JSON type is better for indexing and sorting within JSON objects and arrays in a SELECT statement. Also when you store a value with JSONB() in a JSON type field the UPDATE statement affects only the json fields that have to be updated. You no longer have to replace the whole JSON field value. The functionality seems to be on par with the native JSON handling in Postgres.

The point of this post was to show that when you have a table with JSON datatype and you want to retrieve the whole JSON field as text inside Lazarus, the TSQLQuery object only returns a string field with 1021 chars.

About the autoincrement for primary key - understood.

Thank you for your feedback.


« Last Edit: September 14, 2024, 05:36:55 pm by strubri »

 

TinyPortal © 2005-2018