forked from synopse/mORMot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmORMotSQLite3.pas
More file actions
2560 lines (2414 loc) · 97.3 KB
/
mORMotSQLite3.pas
File metadata and controls
2560 lines (2414 loc) · 97.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// SQLite3 embedded Database engine used as the mORMot SQL kernel
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotSQLite3;
{
This file is part of Synopse mORMot framework.
Synopse mORMot framework. Copyright (c) Arnaud Bouchez
Synopse Informatique - https://synopse.info
*** BEGIN LICENSE BLOCK *****
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.
The Original Code is Synopse mORMot framework.
The Initial Developer of the Original Code is Arnaud Bouchez.
Portions created by the Initial Developer are Copyright (c)
the Initial Developer. All Rights Reserved.
Contributor(s):
- Ondrej
- Mario Moretti
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****
}
interface
{$I Synopse.inc} // define HASINLINE CPU32 CPU64 WITHLOG
uses
{$ifdef MSWINDOWS}
Windows,
{$else}
{$ifdef FPC}
SynFPCLinux,
BaseUnix,
{$endif}
{$ifdef KYLIX3}
Types,
LibC,
SynKylix,
{$endif}
{$endif}
SysUtils,
Classes,
{$ifndef LVCL}
SyncObjs, // for TCriticalSection inlining
Contnrs,
{$endif}
SynZip,
SynCommons,
SynLog,
SynSQLite3,
SynTable,
mORMot;
{.$define WITHUNSAFEBACKUP}
{ define this if you really need the old blocking TSQLRestServerDB backup methods
- those methods are deprecated - you should use DB.BackupBackground() instead }
{ ****************** SQLite3 database used as kernel of our mORMot framework }
type
/// Execute a SQL statement in the local SQLite3 database engine, and get
// result in memory
// - all DATA (even the BLOB fields) is converted into UTF-8 TEXT
// - uses a TSQLTableJSON internaly: faster than sqlite3_get_table()
// (less memory allocation/fragmentation) and allows efficient caching
TSQLTableDB = class(TSQLTableJSON)
private
public
/// Execute a SQL statement, and init TSQLTable fields
// - FieldCount=0 if no result is returned
// - the BLOB data is converted into TEXT: you have to retrieve it with
// a special request explicitely (note that JSON format returns BLOB data)
// - uses a TSQLTableJSON internaly: all currency is transformed to its floating
// point TEXT representation, and allows efficient caching
// - if the SQL statement is in the DB cache, it's retrieved from its cached
// value: our JSON parsing is a lot faster than SQLite3 engine itself,
// and uses less memory
// - will raise an ESQLException on any error
constructor Create(aDB: TSQLDatabase; const Tables: array of TSQLRecordClass;
const aSQL: RawUTF8; Expand: boolean); reintroduce;
end;
/// class-reference type (metaclass) of a REST server using SQLite3 as main engine
TSQLRestServerDBClass = class of TSQLRestServerDB;
TSQLVirtualTableModuleServerDB = class;
/// REST server with direct access to a SQLite3 database
// - caching is handled at TSQLDatabase level
// - SQL statements for record retrieval from ID are prepared for speed
TSQLRestServerDB = class(TSQLRestServer)
private
/// access to the associated SQLite3 database engine
fDB: TSQLDataBase;
/// initialized by Create(aModel,aDBFileName)
fOwnedDB: TSQLDataBase;
/// prepared statements with parameters for faster SQLite3 execution
// - used for SQL code with :(%): internal parameters
fStatementCache: TSQLStatementCached;
/// used during GetAndPrepareStatement() execution (run in global lock)
fStatement: PSQLRequest;
fStaticStatement: TSQLRequest;
fStatementTimer: PPrecisionTimer;
fStatementMonitor: TSynMonitor;
fStaticStatementTimer: TPrecisionTimer;
fStatementSQL: RawUTF8;
fStatementGenericSQL: RawUTF8;
fStatementMaxParam: integer;
fStatementLastException: RawUTF8;
fStatementTruncateSQLLogLen: integer;
fStatementPreparedSelectQueryPlan: boolean;
/// check if a VACUUM statement is possible
// - VACUUM in fact DISCONNECT all virtual modules (sounds like a SQLite3
// design problem), so calling it during process could break the engine
// - if you can safely run VACUUM, returns TRUE and release all active
// SQL statements (otherwise VACUUM will fail)
// - if there are some static virtual tables, returns FALSE and do nothing:
// in this case, VACUUM will be a no-op
function PrepareVacuum(const aSQL: RawUTF8): boolean;
protected
fBatchMethod: TSQLURIMethod;
fBatchOptions: TSQLRestBatchOptions;
fBatchTableIndex: integer;
fBatchID: TIDDynArray;
fBatchIDCount: integer;
fBatchIDMax: TID;
fBatchValues: TRawUTF8DynArray;
fBatchValuesCount: integer;
constructor RegisteredClassCreateFrom(aModel: TSQLModel;
aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); override;
/// retrieve a TSQLRequest instance in fStatement
// - will set @fStaticStatement if no :(%): internal parameters appear:
// in this case, the TSQLRequest.Close method must be called
// - will set a @fStatementCache[].Statement, after having bounded the
// :(%): parameter values; in this case, TSQLRequest.Close must not be called
// - expect sftBlob, sftBlobDynArray and sftBlobRecord properties
// to be encoded as ':("\uFFF0base64encodedbinary"):'
procedure GetAndPrepareStatement(const SQL: RawUTF8; ForceCacheStatement: boolean);
/// free a static prepared statement on success or from except on E: Exception block
procedure GetAndPrepareStatementRelease(E: Exception=nil; const Msg: ShortString='';
ForceBindReset: boolean=false);
/// create or retrieve from the cache a TSQLRequest instance in fStatement
// - called e.g. by GetAndPrepareStatement()
procedure PrepareStatement(Cached: boolean);
/// reset the cache if necessary
procedure SetNoAJAXJSON(const Value: boolean); override;
{$ifdef WITHLOG}
/// overriden method which will also set the DB.LogClass
procedure SetLogClass(aClass: TSynLogClass); override;
{$endif}
/// overridden methods for direct sqlite3 database engine call:
function MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; override;
function MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
function MainEngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
function MainEngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
function MainEngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
function MainEngineDeleteWhere(TableModelIndex: Integer; const SQLWhere: RawUTF8;
const IDs: TIDDynArray): boolean; override;
function MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID;
BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
function MainEngineUpdateBlob(TableModelIndex: integer; aID: TID;
BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
function MainEngineUpdateField(TableModelIndex: integer;
const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
function MainEngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
const FieldName: RawUTF8; Increment: Int64): boolean; override;
function EngineExecute(const aSQL: RawUTF8): boolean; override;
procedure InternalStat(Ctxt: TSQLRestServerURIContext; W: TTextWriter); override;
procedure InternalInfo(var info: TDocVariantData); override;
/// execute one SQL statement
// - intercept any DB exception and return false on error, true on success
// - optional LastInsertedID can be set (if ValueInt/ValueUTF8 are nil) to
// retrieve the proper ID when aSQL is an INSERT statement (thread safe)
// - optional LastChangeCount can be set (if ValueInt/ValueUTF8 are nil) to
// retrieve the modified row count when aSQL is an UPDATE statement (thread safe)
function InternalExecute(const aSQL: RawUTF8; ForceCacheStatement: boolean;
ValueInt: PInt64=nil; ValueUTF8: PRawUTF8=nil; ValueInts: PInt64DynArray=nil;
LastInsertedID: PInt64=nil; LastChangeCount: PInteger=nil): boolean;
// overridden method returning TRUE for next calls to EngineAdd
// will properly handle operations until InternalBatchStop is called
function InternalBatchStart(Method: TSQLURIMethod;
BatchOptions: TSQLRestBatchOptions): boolean; override;
// internal method called by TSQLRestServer.RunBatch() to process fast
// multi-INSERT statements to the SQLite3 engine
procedure InternalBatchStop; override;
public
/// begin a transaction (implements REST BEGIN Member)
// - to be used to speed up some SQL statements like Insert/Update/Delete
// - must be ended with Commit on success
// - must be aborted with Rollback if any SQL statement failed
// - return true if no transaction is active, false otherwise
function TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal=1): boolean; override;
/// end a transaction (implements REST END Member)
// - write all pending SQL statements to the disk
procedure Commit(SessionID: cardinal=1; RaiseException: boolean=false); override;
/// abort a transaction (implements REST ABORT Member)
// - restore the previous state of the database, before the call to TransactionBegin
procedure RollBack(SessionID: cardinal=1); override;
/// overridden method for direct SQLite3 database engine call
// - it will update all BLOB fields at once, in one SQL statement
function UpdateBlobFields(Value: TSQLRecord): boolean; override;
/// overridden method for direct SQLite3 database engine call
// - it will retrieve all BLOB fields at once, in one SQL statement
function RetrieveBlobFields(Value: TSQLRecord): boolean; override;
{$ifndef KYLIX3}
/// backup of the opened Database into an external stream (e.g. a file,
// compressed or not)
// - DEPRECATED: use DB.BackupBackground() instead
// - this method doesn't use the SQLite Online Backup API, but low-level
// database file copy which may lock the database process if the data
// is consistent - consider using DB.BackupBackground() method instead
// - database is closed, VACCUUMed, copied, then reopened
function Backup(Dest: TStream): boolean; deprecated;
/// backup of the opened Database into a .gz compressed file
// - DEPRECATED: use DB.BackupBackground() instead
// - this method doesn't use the SQLite Online Backup API, but low-level
// database file copy which may lock the database process if the data
// is consistent - consider using DB.BackupBackground() method instead
// - database is closed, VACCUUMed, compressed into .gz file, then reopened
// - default compression level is 2, which is very fast, and good enough for
// a database file content: you may change it into the default 6 level
function BackupGZ(const DestFileName: TFileName;
CompressionLevel: integer=2): boolean; deprecated;
{$endif}
/// restore a database content on the fly
// - database is closed, source DB file is replaced by the supplied content,
// then reopened
// - there are cases where this method will fail and return FALSE: consider
// shuting down the server, replace the file, then relaunch the server instead
function Restore(const ContentToRestore: RawByteString): boolean;
/// restore a database content on the fly, from a .gz compressed file
// - database is closed, source DB file is replaced by the supplied content,
// then reopened
// - there are cases where this method will fail and return FALSE: consider
// shuting down the server, replace the file, then relaunch the server instead
function RestoreGZ(const BackupFileName: TFileName): boolean;
/// used e.g. by IAdministratedDaemon to implement "pseudo-SQL" commands
procedure AdministrationExecute(const DatabaseName,SQL: RawUTF8;
var result: TServiceCustomAnswer); override;
/// retrieves the per-statement detailed timing, as a TDocVariantData
procedure ComputeDBStats(out result: variant); overload;
/// retrieves the per-statement detailed timing, as a TDocVariantData
function ComputeDBStats: variant; overload;
/// initialize the associated DB connection
// - called by Create and on Backup/Restore just after DB.DBOpen
// - will register all *_in() functions for available TSQLRecordRTree
// - will register all modules for available TSQLRecordVirtualTable*ID
// with already registered modules via RegisterVirtualTableModule()
// - you can override this method to call e.g. DB.RegisterSQLFunction()
procedure InitializeEngine; virtual;
/// call this method when the internal DB content is known to be invalid
// - by default, all REST/CRUD requests and direct SQL statements are
// scanned and identified as potentially able to change the internal SQL/JSON
// cache used at SQLite3 database level; but some virtual tables (e.g.
// TSQLRestStorageExternal classes defined in SQLite3DB) could flush
// the database content without proper notification
// - this overridden implementation will call TSQLDataBase.CacheFlush method
procedure FlushInternalDBCache; override;
/// call this method to flush the internal SQL prepared statements cache
// - you should not have to flush the cache, only e.g. before a DROP TABLE
// - in all cases, running this method would never harm, nor be slow
procedure FlushStatementCache;
/// execute one SQL statement, and apply an Event to every record
// - lock the database during the run
// - call a fast "stored procedure"-like method for each row of the request;
// this method must use low-level DB access in any attempt to modify the
// database (e.g. a prepared TSQLRequest with Reset+Bind+Step), and not
// the TSQLRestServerDB.Engine*() methods which include a Lock(): this Lock()
// is performed by the main loop in EngineExecute() and any attempt to
// such high-level call will fail into an endless loop
// - caller may use a transaction in order to speed up StoredProc() writing
// - intercept any DB exception and return false on error, true on success
function StoredProcExecute(const aSQL: RawUTF8; StoredProc: TOnSQLStoredProc): boolean;
public
/// initialize a REST server with a SQLite3 database
// - any needed TSQLVirtualTable class should have been already registered
// via the RegisterVirtualTableModule() method
constructor Create(aModel: TSQLModel; aDB: TSQLDataBase;
aHandleUserAuthentication: boolean=false; aOwnDB: boolean=false); reintroduce; overload; virtual;
/// initialize a REST server with a database, by specifying its filename
// - TSQLRestServerDB will initialize a owned TSQLDataBase, and free it on Destroy
// - if specified, the password will be used to cypher this file on disk
// (the main SQLite3 database file is encrypted, not the wal file during run)
// - it will then call the other overloaded constructor to initialize the server
constructor Create(aModel: TSQLModel; const aDBFileName: TFileName;
aHandleUserAuthentication: boolean=false; const aPassword: RawUTF8='';
aDefaultCacheSize: integer=10000; aDefaultPageSize: integer=4096); reintroduce; overload;
/// initialize a REST server with a database, and a temporary Database Model
// - a Model will be created with supplied tables, and owned by the server
// - if you instantiate a TSQLRestServerFullMemory or TSQLRestServerDB
// with this constructor, an in-memory engine will be created, with
// enough abilities to run regression tests, for instance
constructor CreateWithOwnModel(const aTables: array of TSQLRecordClass;
const aDBFileName: TFileName; aHandleUserAuthentication: boolean=false;
const aRoot: RawUTF8='root'; const aPassword: RawUTF8='';
aDefaultCacheSize: integer=10000; aDefaultPageSize: integer=4096); overload;
/// initialize a REST server with an in-memory SQLite3 database
// - could be used for test purposes
constructor Create(aModel: TSQLModel; aHandleUserAuthentication: boolean=false); overload; override;
/// initialize a REST server with an in-memory SQLite3 database and a
// temporary Database Model
// - could be used for test purposes
constructor CreateWithOwnModel(const aTables: array of TSQLRecordClass;
aHandleUserAuthentication: boolean=false); overload;
/// close database and free used memory
destructor Destroy; override;
/// save the TSQLRestServerDB properties into a persistent storage object
// - RegisteredClassCreateFrom() will expect Definition.DatabaseName to store
// the DBFileName, and optionally encrypt the file using Definition.Password
procedure DefinitionTo(Definition: TSynConnectionDefinition); override;
/// Missing tables are created if they don't exist yet for every TSQLRecord
// class of the Database Model
// - you must call explicitely this before having called StaticDataCreate()
// - all table description (even Unique feature) is retrieved from the Model
// - this method also create additional fields, if the TSQLRecord definition
// has been modified; only field adding is available, field renaming or
// field deleting are not allowed in the FrameWork (in such cases, you must
// create a new TSQLRecord type)
procedure CreateMissingTables(user_version: cardinal=0;
Options: TSQLInitializeTableOptions=[]); override;
/// search for the last inserted ID in a table
// - will execute not default select max(rowid) from Table, but faster
// $ select rowid from Table order by rowid desc limit 1
function TableMaxID(Table: TSQLRecordClass): TID; override;
/// after how many bytes a sllSQL statement log entry should be truncated
// - default is 0, meaning no truncation
// - typical value is 2048 (2KB), which will avoid any heap allocation
property StatementTruncateSQLLogLen: integer read fStatementTruncateSQLLogLen
write fStatementTruncateSQLLogLen;
/// executes (therefore log) the QUERY PLAN for each prepared statement
property StatementPreparedSelectQueryPlan: boolean
read fStatementPreparedSelectQueryPlan write fStatementPreparedSelectQueryPlan;
published
/// associated database
property DB: TSQLDataBase read fDB;
/// contains some textual information about the latest Exception raised
// during SQL statement execution
property StatementLastException: RawUTF8 read fStatementLastException;
end;
/// REST client with direct access to a SQLite3 database
// - a hidden TSQLRestServerDB server is created and called internaly
TSQLRestClientDB = class(TSQLRestClientURI)
private
// use internaly a TSQLRestServerDB to access data in the proper JSON format
fServer: TSQLRestServerDB;
fOwnedServer: TSQLRestServerDB;
fOwnedDB: TSQLDataBase;
fInternalHeader: RawUTF8;
function getDB: TSQLDataBase;
protected
/// method calling the RESTful server fServer
procedure InternalURI(var Call: TSQLRestURIParams); override;
/// overridden protected method do nothing (direct DB access has no connection)
function InternalCheckOpen: boolean; override;
/// overridden protected method do nothing (direct DB access has no connection)
procedure InternalClose; override;
public
/// initializes the class, and creates an internal TSQLRestServerDB to
// internaly answer to the REST queries
// - aServerClass could be TSQLRestServerDB by default
constructor Create(aClientModel, aServerModel: TSQLModel; aDB: TSQLDataBase;
aServerClass: TSQLRestServerDBClass;
aHandleUserAuthentication: boolean=false); reintroduce; overload;
/// same as above, from a SQLite3 filename specified
// - an internal TSQLDataBase will be created internaly and freed on Destroy
// - aServerClass could be TSQLRestServerDB by default
// - if specified, the password will be used to cypher this file on disk
// (the main SQLite3 database file is encrypted, not the wal file during run)
constructor Create(aClientModel, aServerModel: TSQLModel; const aDBFileName: TFileName;
aServerClass: TSQLRestServerDBClass; aHandleUserAuthentication: boolean=false;
const aPassword: RawUTF8=''; aDefaultCacheSize: integer=10000); reintroduce; overload;
/// initialize the class, for an existing TSQLRestServerDB
// - the client TSQLModel will be cloned from the server's one
// - the TSQLRestServerDB and TSQLDatabase instances won't be managed by the
// client, but will access directly to the server
constructor Create(aRunningServer: TSQLRestServerDB); reintroduce; overload;
/// release the server
destructor Destroy; override;
/// retrieve a list of members as a TSQLTable (implements REST GET Collection)
// - this overridden method call directly the database to get its result,
// without any URI() call, but with use of DB JSON cache if available
// - other TSQLRestClientDB methods use URI() function and JSON conversion
// of only one record properties values, which is very fast
function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8='ID';
const SQLWhere: RawUTF8=''): TSQLTableJSON; override;
/// associated Server
property Server: TSQLRestServerDB read fServer;
/// associated database
property DB: TSQLDataBase read getDB;
end;
/// define a Virtual Table module for a stand-alone SQLite3 engine
// - it's not needed to free this instance: it will be destroyed by the SQLite3
// engine together with the DB connection
TSQLVirtualTableModuleSQLite3 = class(TSQLVirtualTableModule)
protected
fDB: TSQLDataBase;
/// used internaly to register the module to the SQLite3 engine
fModule: TSQLite3Module;
public
/// initialize the module for a given DB connection
// - internally set fModule and call sqlite3_create_module_v2(fModule)
// - will raise EBusinessLayerException if aDB is incorrect, or SetDB() has
// already been called for this module
// - will call sqlite3_check() to raise the corresponding ESQLite3Exception
// - in case of success (no exception), the SQLite3 engine will release the
// module by itself; but in case of error (an exception is raised), it is
// up to the caller to intercept it via a try..except and free the
// TSQLVirtualTableModuleSQLite3 instance
procedure Attach(aDB: TSQLDataBase);
/// retrieve the file name to be used for a specific Virtual Table
// - overridden method returning a file located in the DB file folder, and
// '' if the main DB was created as SQLITE_MEMORY_DATABASE_NAME (i.e.
// ':memory:' so that no file should be written)
// - of course, if a custom FilePath property value is specified, it will be
// used, even if the DB is created as SQLITE_MEMORY_DATABASE_NAME
function FileName(const aTableName: RawUTF8): TFileName; override;
/// the associated SQLite3 database connection
property DB: TSQLDataBase read fDB;
end;
/// define a Virtual Table module for a TSQLRestServerDB SQLite3 engine
TSQLVirtualTableModuleServerDB = class(TSQLVirtualTableModuleSQLite3)
public
/// register the Virtual Table to the database connection of a TSQLRestServerDB server
// - in case of an error, an excepton will be raised
constructor Create(aClass: TSQLVirtualTableClass; aServer: TSQLRestServer); override;
end;
/// REST storage sharded over several SQlite3 instances
// - numerotated '*0000.dbs' SQLite3 files would contain the sharded data
// - here *.dbs is used as extension, to avoid any confusion with regular
// SQLite3 database files (*.db or *.db3)
// - when the server is off (e.g. on periodic version upgrade), you may safely
// delete/archive some oldest *.dbs files, for easy and immediate purge of
// your database content: such process would be much faster and cleaner than
// regular "DELETE FROM TABLE WHERE ID < ?" + "VACUUM" commands
TSQLRestStorageShardDB = class(TSQLRestStorageShard)
protected
fShardRootFileName: TFileName;
fSynchronous: TSQLSynchronousMode;
fInitShardsIsLast: boolean;
fCacheSizePrevious, fCacheSizeLast: integer;
procedure InitShards; override;
function InitNewShard: TSQLRest; override;
function DBFileName(ShardIndex: Integer): TFileName; virtual;
public
/// initialize the table storage redirection for sharding over SQLite3 DB
// - if no aShardRootFileName is set, the executable folder and stored class
// table name would be used
// - typical use may be:
// ! Server.StaticDataAdd(TSQLRestStorageShardDB.Create(TSQLRecordSharded,Server,500000))
// - you may define some low-level tuning of SQLite3 process via aSynchronous
// / aCacheSizePrevious / aCacheSizeLast / aMaxShardCount parameters, if
// the default smOff / 1MB / 2MB / 100 values are not enough
constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer;
aShardRange: TID; aOptions: TSQLRestStorageShardOptions=[];
const aShardRootFileName: TFileName=''; aMaxShardCount: integer=100;
aSynchronous: TSQLSynchronousMode=smOff;
aCacheSizePrevious: integer=250; aCacheSizeLast: integer=500); reintroduce; virtual;
published
/// associated file name for the SQLite3 database files
// - contains the folder, and root file name for the storage
// - each shard would end with its 4 digits index: actual file name would
// append '0000.dbs' to this ShardRootFileName
property ShardRootFileName: TFileName read fShardRootFileName;
end;
/// initialize a Virtual Table Module for a specified database
// - to be used for low-level access to a virtual module, e.g. with
// TSQLVirtualTableLog
// - when using our ORM, you should call TSQLModel.VirtualTableRegister()
// instead to associate a TSQLRecordVirtual class to a module
// - returns the created TSQLVirtualTableModule instance (which will be a
// TSQLVirtualTableModuleSQLite3 instance in fact)
// - will raise an exception of failure
function RegisterVirtualTableModule(aModule: TSQLVirtualTableClass;
aDatabase: TSQLDataBase): TSQLVirtualTableModule;
implementation
{$ifdef SQLVIRTUALLOGS}
uses
mORMotDB;
{$endif SQLVIRTUALLOGS}
{ TSQLTableDB }
constructor TSQLTableDB.Create(aDB: TSQLDataBase; const Tables: array of TSQLRecordClass;
const aSQL: RawUTF8; Expand: boolean);
var JSONCached: RawUTF8;
R: TSQLRequest;
n: PtrInt;
begin
if aDB=nil then
exit;
JSONCached := aDB.LockJSON(aSQL,@n);
if JSONCached='' then // not retrieved from cache -> call SQLite3 engine
try // faster than sqlite3_get_table(): memory is allocated as a whole
n := 0;
JSONCached := R.ExecuteJSON(aDB.DB,aSQL,Expand,@n); // Expand=true for AJAX
inherited CreateFromTables(Tables,aSQL,JSONCached);
Assert(n=fRowCount);
finally
aDB.UnLockJSON(JSONCached,n);
end
else begin
inherited CreateFromTables(Tables,aSQL,JSONCached);
Assert(n=fRowCount);
end;
end;
{ TSQLRestServerDB }
procedure TSQLRestServerDB.PrepareStatement(Cached: boolean);
var wasPrepared: boolean;
timer: PPPrecisionTimer;
begin
fStaticStatementTimer.Start;
if not Cached then begin
fStaticStatement.Prepare(DB.DB,fStatementGenericSQL);
fStatementGenericSQL := '';
fStatement := @fStaticStatement;
fStatementTimer := @fStaticStatementTimer;
fStatementMonitor := nil;
exit;
end;
if mlSQLite3 in StatLevels then
timer := @fStatementTimer else
timer := nil;
fStatement := fStatementCache.Prepare(fStatementGenericSQL,@wasPrepared,timer,@fStatementMonitor);
if wasPrepared then begin
InternalLog('prepared % % %', [fStaticStatementTimer.Stop,
DB.FileNameWithoutPath,fStatementGenericSQL],sllDB);
if fStatementPreparedSelectQueryPlan then
DB.ExecuteJSON('explain query plan '+
StringReplaceChars(fStatementGenericSQL,'?','1'), {expand=}true);
end;
if timer=nil then begin
fStaticStatementTimer.Start;
fStatementTimer := @fStaticStatementTimer;
fStatementMonitor := nil;
end;
end;
procedure TSQLRestServerDB.GetAndPrepareStatement(const SQL: RawUTF8;
ForceCacheStatement: boolean);
var i, sqlite3param: integer;
Types: TSQLParamTypeDynArray;
Nulls: TSQLFieldBits;
Values: TRawUTF8DynArray;
begin
// prepare statement
fStatementSQL := SQL;
fStatementGenericSQL := ExtractInlineParameters(SQL,Types,Values,fStatementMaxParam,Nulls);
PrepareStatement(ForceCacheStatement or (fStatementMaxParam<>0));
// bind parameters
if fStatementMaxParam=0 then
exit; // no valid :(...): inlined parameter found -> manual bind
sqlite3param := sqlite3.bind_parameter_count(fStatement^.Request);
if sqlite3param<>fStatementMaxParam then
raise EORMException.CreateUTF8(
'%.GetAndPrepareStatement(%) recognized % params, and % for SQLite3',
[self,fStatementGenericSQL,fStatementMaxParam,sqlite3param]);
for i := 0 to fStatementMaxParam-1 do
if i in Nulls then
fStatement^.BindNull(i+1) else
case Types[i] of
sptDateTime, // date/time are stored as ISO-8601 TEXT in SQLite3
sptText: fStatement^.Bind(i+1,Values[i]);
sptBlob: fStatement^.BindBlob(i+1,Values[i]);
sptInteger: fStatement^.Bind(i+1,GetInt64(pointer(Values[i])));
sptFloat: fStatement^.Bind(i+1,GetExtended(pointer(Values[i])));
end;
end;
procedure TSQLRestServerDB.GetAndPrepareStatementRelease(E: Exception;
const Msg: ShortString; ForceBindReset: boolean);
var
tmp: TSynTempBuffer;
P: PAnsiChar;
begin
try
if fStatementTimer<>nil then begin
if fStatementMonitor<>nil then
fStatementMonitor.ProcessEnd else
fStatementTimer^.Pause;
if E=nil then
if (fStatementTruncateSQLLogLen > 0) and
(length(fStatementSQL) > fStatementTruncateSQLLogLen) then begin
tmp.Init(pointer(fStatementSQL),fStatementTruncateSQLLogLen);
P := tmp.buf;
PCardinal(P+fStatementTruncateSQLLogLen-3)^ := ord('.')+ord('.')shl 8+ord('.')shl 16;
InternalLog('% % % len=%',[fStatementTimer^.LastTime,Msg,P,length(fStatementSQL)],sllSQL);
tmp.Done;
end else
InternalLog('% % %',[fStatementTimer^.LastTime,Msg,fStatementSQL],sllSQL) else
InternalLog('% for % // %',[E,fStatementSQL,fStatementGenericSQL],sllError);
fStatementTimer := nil;
end;
fStatementMonitor := nil;
finally
if fStatement<>nil then begin
if fStatement=@fStaticStatement then
fStaticStatement.Close else
if (fStatementMaxParam<>0) or ForceBindReset then
fStatement^.BindReset; // release bound RawUTF8 ASAP
fStatement := nil;
end;
fStatementSQL := '';
fStatementGenericSQL := '';
fStatementMaxParam := 0;
if E<>nil then
FormatUTF8('% %',[E,ObjectToJSONDebug(E)],fStatementLastException);
end;
end;
procedure TSQLRestServerDB.FlushStatementCache;
begin
DB.Lock;
try
fStatementCache.ReleaseAllDBStatements;
finally
DB.Unlock;
end;
end;
function TSQLRestServerDB.TableMaxID(Table: TSQLRecordClass): TID;
var SQL: RawUTF8;
begin
if StaticTable[Table]<>nil then
result := inherited TableMaxID(Table) else begin
SQL := 'select rowid from '+Table.SQLTableName+' order by rowid desc limit 1';
if not InternalExecute(SQL,true,PInt64(@result)) then
result := 0;
end;
end;
function TSQLRestServerDB.MainEngineAdd(TableModelIndex: integer;
const SentData: RawUTF8): TID;
var Props: TSQLRecordProperties;
SQL: RawUTF8;
Decoder: TJSONObjectDecoder;
begin
result := 0;
if TableModelIndex<0 then
exit;
Props := fModel.TableProps[TableModelIndex].Props;
SQL := Props.SQLTableName;
if fBatchMethod<>mNone then begin
result := 0; // indicates error
if SentData='' then
InternalLog('BATCH with MainEngineAdd(%,SentData="") -> '+
'DEFAULT VALUES not implemented',[SQL],sllError) else
if (fBatchMethod=mPOST) and (fBatchIDMax>=0) and
((fBatchTableIndex<0) or (fBatchTableIndex=TableModelIndex)) then begin
fBatchTableIndex := TableModelIndex;
if JSONGetID(pointer(SentData),result) then begin
if result>fBatchIDMax then
fBatchIDMax := result;
end else begin
if fBatchIDMax=0 then begin
fBatchIDMax := TableMaxID(Props.Table);
if fBatchIDMax<0 then
exit; // will force error for whole BATCH block
end;
inc(fBatchIDMax);
result := fBatchIDMax;
end;
AddID(fBatchID,fBatchIDCount,result);
AddRawUTF8(fBatchValues,fBatchValuesCount,SentData);
end;
exit;
end;
SQL := 'INSERT INTO '+SQL;
if trim(SentData)='' then
SQL := SQL+' DEFAULT VALUES;' else begin
JSONGetID(pointer(SentData),result);
Decoder.Decode(SentData,nil,pInlined,result,false);
if Props.RecordVersionField<>nil then
InternalRecordVersionHandle(
soInsert,TableModelIndex,decoder,Props.RecordVersionField);
SQL := SQL+Decoder.EncodeAsSQL(false)+';';
end;
if InternalExecute(SQL,true,nil,nil,nil,PInt64(@result)) then
InternalUpdateEvent(seAdd,TableModelIndex,result,SentData,nil);
end;
procedure InternalRTreeIn(Context: TSQLite3FunctionContext;
argc: integer; var argv: TSQLite3ValueArray); cdecl;
var aRTree: TSQLRecordRTreeClass;
BlobA, BlobB: pointer;
begin
if argc<>2 then begin
ErrorWrongNumberOfArgs(Context);
exit;
end;
aRTree := sqlite3.user_data(Context);
BlobA := sqlite3.value_blob(argv[0]);
BlobB := sqlite3.value_blob(argv[1]);
if (aRTree=nil) or (BlobA=nil) or (BlobB=nil) then
sqlite3.result_error(Context,'invalid call') else
sqlite3.result_int64(Context,byte(aRTree.ContainedIn(BlobA^,BlobB^)));
end;
constructor TSQLRestServerDB.Create(aModel: TSQLModel;
aHandleUserAuthentication: boolean);
begin
Create(aModel,SQLITE_MEMORY_DATABASE_NAME,aHandleUserAuthentication);
end;
constructor TSQLRestServerDB.Create(aModel: TSQLModel; aDB: TSQLDataBase;
aHandleUserAuthentication, aOwnDB: boolean);
begin
fStatementCache.Init(aDB.DB);
aDB.UseCache := true; // we better use caching in this JSON oriented use
fDB := aDB;
if aOwnDB then
fOwnedDB := fDB;
if fDB.InternalState=nil then begin // should be done once
InternalState := 1;
fDB.InternalState := @InternalState; // to update TSQLRestServerDB.InternalState
end;
inherited Create(aModel,aHandleUserAuthentication);
InitializeEngine;
end;
{$ifdef WITHLOG}
procedure TSQLRestServerDB.SetLogClass(aClass: TSynLogClass);
begin
inherited;
if DB<>nil then
DB.Log := aClass; // ensure low-level SQLite3 engine will share the same log
end;
{$endif}
procedure TSQLRestServerDB.InitializeEngine;
var i: integer;
module: TSQLVirtualTableClass;
registered: array of TSQLVirtualTableClass;
begin
for i := 0 to high(Model.TableProps) do
case Model.TableProps[i].Kind of
rRTree, rRTreeInteger: // register all *_in() SQL functions
sqlite3_check(DB.DB,sqlite3.create_function_v2(DB.DB,
pointer(TSQLRecordRTreeClass(Model.Tables[i]).RTreeSQLFunctionName),
2,SQLITE_ANY,Model.Tables[i],InternalRTreeIn,nil,nil,nil));
rCustomForcedID, rCustomAutoID: begin
module := Model.VirtualTableModule(Model.Tables[i]);
if (module<>nil) and (PtrArrayFind(registered,module)<0) then begin
TSQLVirtualTableModuleServerDB.Create(module,self);
PtrArrayAdd(registered,module); // register it once for this DB
end;
end;
end;
end;
constructor TSQLRestServerDB.Create(aModel: TSQLModel; const aDBFileName: TFileName;
aHandleUserAuthentication: boolean; const aPassword: RawUTF8;
aDefaultCacheSize, aDefaultPageSize: integer);
begin
fOwnedDB := TSQLDataBase.Create(aDBFileName,aPassword,0,aDefaultCacheSize,aDefaultPageSize);
// fOwnedDB.Free done in Destroy
Create(aModel,fOwnedDB,aHandleUserAuthentication);
end;
constructor TSQLRestServerDB.CreateWithOwnModel(const aTables: array of TSQLRecordClass;
const aDBFileName: TFileName; aHandleUserAuthentication: boolean;
const aRoot, aPassword: RawUTF8; aDefaultCacheSize, aDefaultPageSize: integer);
begin
Create(TSQLModel.Create(aTables,aRoot),aDBFileName,aHandleUserAuthentication,
aPassword,aDefaultCacheSize,aDefaultPageSize);
fModel.Owner := self;
end;
constructor TSQLRestServerDB.CreateWithOwnModel(const aTables: array of TSQLRecordClass;
aHandleUserAuthentication: boolean);
begin
Create(TSQLModel.Create(aTables),aHandleUserAuthentication);
fModel.Owner := self;
end;
procedure TSQLRestServerDB.CreateMissingTables(user_version: cardinal;
Options: TSQLInitializeTableOptions);
var t,f,nt,nf: integer;
TableNamesAtCreation, aFields: TRawUTF8DynArray;
TableJustCreated: TSQLFieldTables;
aSQL: RawUTF8;
begin
if DB.TransactionActive then
raise EBusinessLayerException.Create('CreateMissingTables in transaction');
fDB.GetTableNames(TableNamesAtCreation);
nt := length(TableNamesAtCreation);
QuickSortRawUTF8(TableNamesAtCreation,nt,nil,@StrIComp);
{$ifdef WITHLOG}
fLogFamily.SynLog.Log(sllDB,'CreateMissingTables on %',[fDB],self);
fLogFamily.SynLog.Log(sllDB,'GetTables',TypeInfo(TRawUTF8DynArray),TableNamesAtCreation,self);
{$endif}
FillcharFast(TableJustCreated,sizeof(TSQLFieldTables),0);
try
// create not static and not existing tables
for t := 0 to high(Model.Tables) do
if ((fStaticData=nil) or (fStaticData[t]=nil)) then
// this table is not static -> check if already existing, create if necessary
with Model.TableProps[t], Props do
if not NoCreateMissingTable then
if FastFindPUTF8CharSorted(pointer(TableNamesAtCreation),nt-1,pointer(SQLTableName),@StrIComp)<0 then begin
if not DB.TransactionActive then
DB.TransactionBegin; // make initialization faster by using transaction
DB.Execute(Model.GetSQLCreate(t)); // don't catch exception in constructor
include(TableJustCreated,t); // mark to be initialized below
end else
if not(itoNoCreateMissingField in Options) then begin
// this table is existing: check that all fields exist -> create if necessary
DB.GetFieldNames(aFields,SQLTableName);
nf := length(aFields);
QuickSortRawUTF8(aFields,nf,nil,@StrIComp);
for f := 0 to Fields.Count-1 do
with Fields.List[f] do
if SQLFieldType in COPIABLE_FIELDS then
/// real database columns exist for Simple + Blob fields (not Many)
if FastFindPUTF8CharSorted(pointer(aFields),nf-1,pointer(Name),@StrIComp)<0 then begin
aSQL := Model.GetSQLAddField(t,f);
if aSQL<>'' then begin // need a true field with data
if not DB.TransactionActive then
DB.TransactionBegin; // make initialization faster by using transaction
DB.Execute(aSQL);
end;
Model.Tables[t].InitializeTable(self,Name,Options);
end;
end;
if not DB.TransactionActive then
exit;
// database schema was modified -> update user version in SQLite3 file
if user_version<>0 then
DB.user_version := user_version;
// initialize new tables AFTER creation of ALL tables
if not IsZero(@TableJustCreated,sizeof(TSQLFieldTables)) then
for t := 0 to high(Model.Tables) do
if t in TableJustCreated then
if not(Model.TableProps[t].Kind in IS_CUSTOM_VIRTUAL) or
not TableHasRows(Model.Tables[t]) then // check is really void
Model.Tables[t].InitializeTable(self,'',Options); // '' for table creation
DB.Commit;
except
on E: Exception do begin
DB.RollBack; // will close any active Transaction
raise; // caller must handle exception
end;
end;
end;
function TSQLRestServerDB.MainEngineDelete(TableModelIndex: integer; ID: TID): boolean;
begin
if (TableModelIndex<0) or (ID<=0) then
result := false else begin
// notify BEFORE deletion
InternalUpdateEvent(seDelete,TableModelIndex,ID,'',nil);
result := ExecuteFmt('DELETE FROM % WHERE RowID=:(%):;',
[fModel.TableProps[TableModelIndex].Props.SQLTableName,ID]);
end;
end;
function TSQLRestServerDB.MainEngineDeleteWhere(TableModelIndex: Integer;
const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
var i: integer;
aSQLWhere: RawUTF8;
begin
if (TableModelIndex<0) or (IDs=nil) then
result := false else begin
// notify BEFORE deletion
for i := 0 to high(IDs) do
InternalUpdateEvent(seDelete,TableModelIndex,IDs[i],'',nil);
if IdemPChar(pointer(SQLWhere),'LIMIT ') or
IdemPChar(pointer(SQLWhere),'ORDER BY ') then
// LIMIT is not handled by SQLite3 when built from amalgamation
// see http://www.sqlite.org/compile.html#enable_update_delete_limit
aSQLWhere := Int64DynArrayToCSV(pointer(IDs),length(IDs),'RowID IN (',')') else
aSQLWhere := SQLWhere;
result := ExecuteFmt('DELETE FROM %%',
[fModel.TableProps[TableModelIndex].Props.SQLTableName,SQLFromWhere(aSQLWhere)]);
end;
end;
destructor TSQLRestServerDB.Destroy;
begin
{$ifdef WITHLOG}
with fLogClass.Enter('Destroy %', [fModel.SafeRoot], self) do
{$endif}
try
if (fDB<>nil) and (fDB.InternalState=@InternalState) then
fDB.InternalState := nil; // avoid memory modification on free block
inherited Destroy;
finally
try
fStatementCache.ReleaseAllDBStatements;
finally
fOwnedDB.Free; // do nothing if DB<>fOwnedDB
end;
end;
end;
procedure TSQLRestServerDB.DefinitionTo(Definition: TSynConnectionDefinition);
begin
if Definition=nil then
exit;
inherited; // set Kind
if fDB<>nil then begin
Definition.ServerName := StringToUTF8(fDB.FileName);
Definition.PasswordPlain := fDB.Password;
end;
end;
constructor TSQLRestServerDB.RegisteredClassCreateFrom(aModel: TSQLModel;
aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition);
begin
Create(aModel,UTF8ToString(aDefinition.ServerName),aServerHandleAuthentication,
aDefinition.PasswordPlain);
end;
function TSQLRestServerDB.PrepareVacuum(const aSQL: RawUTF8): boolean;
begin
result := not IdemPChar(Pointer(aSQL),'VACUUM');
if result then
exit;
result := (fStaticVirtualTable=nil) or
IsZero(fStaticVirtualTable,length(fStaticVirtualTable)*sizeof(pointer));
if result then
// VACUUM will fail if there are one or more active SQL statements
fStatementCache.ReleaseAllDBStatements;
end;
function TSQLRestServerDB.InternalExecute(const aSQL: RawUTF8;
ForceCacheStatement: boolean; ValueInt: PInt64; ValueUTF8: PRawUTF8;
ValueInts: PInt64DynArray; LastInsertedID: PInt64; LastChangeCount: PInteger): boolean;
var ValueIntsCount, Res: Integer;
msg: shortstring;
begin
msg := '';
if (self<>nil) and (DB<>nil) then
try
DB.Lock(aSQL);
try
result := true;
if not PrepareVacuum(aSQL) then
exit; // no-op if there are some static virtual tables around
try
GetAndPrepareStatement(aSQL,ForceCacheStatement);
if ValueInts<>nil then begin
ValueIntsCount := 0;
repeat
res := fStatement^.Step;
if res=SQLITE_ROW then
AddInt64(ValueInts^,ValueIntsCount,fStatement^.FieldInt(0));
until res=SQLITE_DONE;
SetLength(ValueInts^,ValueIntsCount);
FormatShort('returned Int64 len=%',[ValueIntsCount],msg);
end else
if (ValueInt=nil) and (ValueUTF8=nil) then begin
// default execution: loop through all rows