forked from synopse/mORMot2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmormot.orm.sqlite3.pas
2905 lines (2754 loc) · 100 KB
/
mormot.orm.sqlite3.pas
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
/// ORM Types and Classes for direct SQLite3 Database Access
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.orm.sqlite3;
{
*****************************************************************************
ORM SQLite3 Database Access using mormot.db.raw.sqlite3 unit
- TOrmTableDB as Efficient ORM-Aware TOrmTable
- TOrmVirtualTableModuleServerDB for SQLite3 Virtual Tables
- TRestStorageShardDB for REST Storage Sharded Over SQlite3 Files
- TRestStorageMultiDB for REST Storage of Multi-User SQlite3 Files
- TRestOrmServerDB REST Server ORM Engine over SQLite3
- TRestOrmClientDB REST Client ORM Engine over SQLite3
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
uses
sysutils,
classes,
variants,
contnrs,
mormot.core.base,
mormot.core.os,
mormot.core.buffers,
mormot.core.unicode,
mormot.core.text,
mormot.core.datetime,
mormot.core.variants,
mormot.core.data,
mormot.core.rtti,
mormot.core.json,
mormot.core.threads,
mormot.crypt.core,
mormot.crypt.jwt,
mormot.core.perf,
mormot.core.search, // for TRestStorageShardDB FindFiles()
mormot.crypt.secure,
mormot.core.log,
mormot.orm.base,
mormot.orm.core,
mormot.orm.rest,
mormot.orm.client,
mormot.orm.server,
mormot.orm.storage,
mormot.db.core,
mormot.rest.core,
mormot.rest.server,
mormot.db.raw.sqlite3;
{ *********** TOrmTableDB as Efficient ORM-Aware TOrmTable }
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 TOrmTableJson internally: faster than sqlite3_get_table()
// (less memory allocation/fragmentation) and allows efficient caching
TOrmTableDB = class(TOrmTableJson)
private
public
/// Execute a SQL statement, and init TOrmTable fields
// - FieldCount=0 if no result is returned
// - the BLOB data is converted into TEXT: you have to retrieve it with
// a special request explicitly (note that JSON format returns BLOB data)
// - uses a TOrmTableJson internally: 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 the SQLite3 engine
// itself, and uses less memory
// - will raise an Exception on any error
constructor Create(aDB: TSqlDatabase; const Tables: array of TOrmClass;
const aSql: RawUtf8; Expand: boolean); reintroduce;
end;
{ *********** TOrmVirtualTableModuleServerDB for SQLite3 Virtual Tables }
type
/// 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
TOrmVirtualTableModuleSQLite3 = class(TOrmVirtualTableModule)
protected
fDB: TSqlDataBase;
/// used internally 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
// TOrmVirtualTableModuleSQLite3 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 TRestOrmServerDB SQLite3 engine
TOrmVirtualTableModuleServerDB = class(TOrmVirtualTableModuleSQLite3)
public
/// register the Virtual Table to the database connection of a TRestOrmServerDB server
// - in case of an error, an excepton will be raised
constructor Create(aClass: TOrmVirtualTableClass; aServer: TRestOrmServer); override;
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: TOrmVirtualTableClass;
aDatabase: TSqlDataBase): TOrmVirtualTableModule;
{ *********** TRestStorageShardDB for REST Storage Sharded Over SQlite3 Files }
type
/// 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
TRestStorageShardDB = class(TRestStorageShard)
protected
fShardRootFileName: TFileName;
fSynchronous: TSqlSynchronousMode;
fInitShardsIsLast: boolean;
fCacheSizePrevious, fCacheSizeLast: integer;
fDBPassword: SpiUtf8;
procedure InitShards; override;
function InitNewShard: TRestOrm; override;
// you can override those methods to customize the stored SQlite3 files
function DBPassword(ShardIndex: integer): SpiUtf8; virtual;
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
// - will also register to the aServer.GetStaticStorage() internal list
// - 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: TOrmClass; aServer: TRestServer;
aShardRange: TID;
aOptions: TRestStorageShardOptions = [];
const aShardRootFileName: TFileName = '';
aMaxShardCount: integer = 100; aSynchronous: TSqlSynchronousMode = smOff;
aCacheSizePrevious: integer = 250; aCacheSizeLast: integer = 500;
const aDBPassword: SpiUtf8 = ''); reintroduce; virtual;
/// finalize the table storage, including Shards[] instances
destructor Destroy; override;
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;
{ *********** TRestStorageMultiDB for REST Storage of Multi-User SQlite3 Files }
type
/// abstract REST storage with several SQLite3 database instances
TRestStorageMultiDB = class(TRestStorageMultiOnDisk)
protected
// overriden to proper create each SQlite3 database
fSynchronous: TSqlSynchronousMode;
fDefaultCacheSize: integer;
function NewDB(aID: TRestStorageMultiDatabaseID): IRestOrmServer; override;
public
/// initialize this REST storage with several SQLite3 database instances
// - aDatabaseIDBits will define how many bits (1-63) are allowed for
// TRestStorageMultiDatabaseID values
// - aSettings instance will be owned by this main class instance
// - aDataFolder, aFilePrefix will be used by overriden NewDB()
// - aModelClasses will be used by the overriden NewModel()
// - aSynchronous/aDefaultCacheSize can override default smOff/2MB
constructor Create(aLog: TSynLogFamily; aDatabaseIDBits: byte;
const aDataFolder, aFilePrefix: TFileName;
const aModelClasses: array of TOrmClass;
aSettings: TRestStorageMultiSettings;
aSynchronous: TSqlSynchronousMode = smOff;
aDefaultCacheSize: integer = 500); reintroduce;
end;
{ *********** TRestOrmServerDB REST ORM Engine over SQLite3 }
type
/// low-level internal structure used by TRestOrmServerDB for its Batch process
TRestOrmServerDBBatch = record
Encoding: TRestBatchEncoding;
Options: TRestBatchOptions;
TableIndex: integer;
ID: TIDDynArray;
IDCount: integer;
IDMax: TID;
Values: TRawUtf8DynArray;
ValuesCount: integer;
Simples: TPUtf8CharDynArray;
SimpleFieldsCount: integer;
SimpleFields: TFieldBits;
UpdateSql: RawUtf8;
UpdateFieldsCount: integer;
Types: array[0..MAX_SQLFIELDS - 1] of TSqlDBFieldType;
PostValues: array[0..MAX_SQLPARAMS - 1] of RawUtf8;
Temp: TSynTempBuffer;
end;
PRestOrmServerDBBatch = ^TRestOrmServerDBBatch;
/// implements TRestServerDB.ORM process for REST server over SQlite3 storage
// - the main engine will be SQLite3, but specific classes of the model could
// be redirected to other TRestStorage instances, e.g. external SQL/NoSQL
TRestOrmServerDB = class(TRestOrmServer)
protected
/// access to the associated SQLite3 database engine
fDB: TSqlDataBase;
/// initialized by Create(aModel,aDBFileName)
fOwnedDB: TSqlDataBase;
fStatementCache: TSqlStatementCached;
/// used during GetAndPrepareStatement() execution (run in global lock)
fStatement: PSqlRequest;
fStaticStatement: TSqlRequest;
fStatementTimer: PPrecisionTimer;
fStatementMonitor: TSynMonitor;
fStaticStatementTimer: TPrecisionTimer;
fStatementSql: RawUtf8;
fStatementLastException: RawUtf8;
fStatementTruncateSqlLogLen: integer;
fStatementDecoder: TExtractInlineParameters;
/// 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
fBatch: PRestOrmServerDBBatch;
fJsonDecoder: TJsonObjectDecoder; // protected by execOrmWrite lock
/// 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); overload;
procedure GetAndPrepareStatementRelease(E: Exception;
const Format: RawUtf8; const Args: array of const;
ForceBindReset: boolean = false); overload;
/// create or retrieve from the cache a TSqlRequest instance in fStatement
// - called e.g. by GetAndPrepareStatement()
procedure PrepareStatement(Cached: boolean);
procedure PrepareCachedStatement(const SQL: RawUtf8; ExpectedParams: integer);
public
/// 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: PRttiProp; out BlobData: RawBlob): boolean; override;
function MainEngineUpdateBlob(TableModelIndex: integer; aID: TID;
BlobField: PRttiProp; const BlobData: RawBlob): 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;
/// 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(Encoding: TRestBatchEncoding;
BatchOptions: TRestBatchOptions): boolean; override;
// internal method called by TRestOrmServer.RunBatch() to process fast
// multi-INSERT statements to the SQLite3 engine
procedure InternalBatchStop; override;
/// internal method called by TRestServer.Batch() to process SIMPLE input
// - overriden for optimized multi-insert of the supplied JSON array values
function InternalBatchDirectSupport(Encoding: TRestBatchEncoding;
RunTableIndex: integer): TRestOrmBatchDirect; override;
/// internal method called by TRestServer.Batch() to process SIMPLE input
// - overriden for optimized multi-insert of the supplied JSON array values
function InternalBatchDirectOne(Encoding: TRestBatchEncoding;
RunTableIndex: integer; const Fields: TFieldBits; Sent: PUtf8Char): TID; override;
/// reset the cache if necessary
procedure SetNoAjaxJson(const Value: boolean); 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: TOrmClass;
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: TOrm): 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: TOrm): boolean; 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 TOrmRTree
// - will register all modules for available TOrmVirtualTable*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.
// TRestStorageExternal 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 TRestOrmServerDB.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;
const StoredProc: TOnSqlStoredProc): boolean;
public
/// initialize a TRest-owned ORM server with an in-memory SQLite3 database
constructor Create(aRest: TRest); overload; override;
/// initialize a TRest-owned ORM server with a given SQLite3 database
// - you should specify a TSqlDataBase and a TRest associated instance
constructor Create(aRest: TRest;
aDB: TSqlDataBase; aOwnDB: boolean); reintroduce; overload; virtual;
/// initialize a stand-alone REST ORM server with a given SQLite3 database
// - you can specify an associated TOrmModel but no TRest
constructor CreateStandalone(aModel: TOrmModel; aRest: TRest;
aDB: TSqlDataBase; aOwnDB: boolean); reintroduce; overload;
/// initialize a stand-alone REST ORM server with a given SQLite3 filename
// - you can specify an associated TOrmModel but no TRest
// - the SQlite3 database instance will be createa as lmExclusive/aSynchronous
constructor CreateStandalone(aModel: TOrmModel; aRest: TRest;
const aDB: TFileName; const aPassword: SpiUtf8 = '';
aSynchronous: TSqlSynchronousMode = smOff;
aDefaultCacheSize: integer = 10000); reintroduce; overload;
/// close any owned database and free used memory
destructor Destroy; override;
/// Missing tables are created if they don't exist yet for every TOrm
// class of the Database Model
// - you must call explicitly this before calling OrmMapInMemory()
// - all table description (even Unique feature) is retrieved from the Model
// - this method also create additional fields, if the TOrm 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 TOrm type)
procedure CreateMissingTables(user_version: cardinal = 0;
Options: TOrmInitializeTableOptions = []); 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: TOrmClass): TID; override;
/// overridden method for direct SQLite3 engine call
function MemberExists(Table: TOrmClass; ID: TID): boolean; override;
/// prepared statements with parameters for faster SQLite3 execution
// - used for SQL code with :(%): internal parameters
property StatementCache: TSqlStatementCached
read fStatementCache;
/// 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;
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;
{ *********** TRestOrmClientDB REST Client ORM Engine over SQLite3 }
type
/// REST ORM client with direct access to a SQLite3 database
// - a hidden TRestOrmDB class is created and called internally
TRestOrmClientDB = class(TRestOrmClientUri)
private
// use internally a TRestServerDB to access data in the proper JSON format
fServer: TRestOrmServerDB;
function GetDB: TSqlDataBase;
public
/// initialize the ORM storage, with the associated ORM Server
constructor Create(aRest: TRest; aServer: TRestOrmServerDB); reintroduce;
/// retrieve a list of members as a TOrmTable (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 TRestClientDB methods use Uri() function and JSON conversion
// of only one record properties values, which is very fast
function List(const Tables: array of TOrmClass;
const SqlSelect: RawUtf8 = 'ID';
const SqlWhere: RawUtf8 = ''): TOrmTable; override;
/// associated ORM Server
property Server: TRestOrmServerDB
read fServer;
/// associated database
property DB: TSqlDataBase
read GetDB;
end;
implementation
{ *********** TOrmTableDB as Efficient ORM-Aware TOrmTable }
{ TOrmTableDB }
constructor TOrmTableDB.Create(aDB: TSqlDatabase;
const Tables: array of TOrmClass; 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
n := 0;
jsoncached := r.ExecuteJson(
aDB.DB, aSql, Expand, @n, aDB.StatementMaxMemory);
// big JSON is faster than sqlite3_get_table(): less heap allocations
inherited CreateFromTables(Tables, aSql, jsoncached);
Assert(n = fRowCount);
finally
aDB.UnLockJson(aSql, jsoncached, n);
end
else
begin
inherited CreateFromTables(Tables, aSql, jsoncached);
Assert(n = fRowCount);
end;
end;
{ *********** TOrmVirtualTableModuleServerDB for SQLite3 Virtual Tables }
// asssociated low-level vt*() SQlite3 wrapper functions
procedure Notify(const Format: RawUtf8; const Args: array of const);
begin
TSynLog.DebuggerNotify(sllWarning, Format, Args);
end;
function vt_Create(DB: TSqlite3DB; pAux: pointer; argc: integer;
const argv: PPUtf8CharArray; var ppVTab: PSqlite3VTab;
var pzErr: PUtf8Char): integer; cdecl;
var
module: TOrmVirtualTableModuleSQLite3 absolute pAux;
table: TOrmVirtualTable;
struct: RawUtf8;
modname: RawUtf8;
begin
if module <> nil then
modname := module.ModuleName;
if (module = nil) or
(module.DB.DB <> DB) or
(StrIComp(pointer(modname), argv[0]) <> 0) then
begin
Notify('vt_Create(%<>%)', [argv[0], modname]);
result := SQLITE_ERROR;
exit;
end;
ppVTab := sqlite3.malloc(SizeOf(TSqlite3VTab));
if ppVTab = nil then
begin
result := SQLITE_NOMEM;
exit;
end;
FillcharFast(ppVTab^, SizeOf(ppVTab^), 0);
try
table := module.TableClass.Create(
module, RawUtf8(argv[2]), argc - 3, @argv[3]);
except
on E: Exception do
begin
ExceptionToSqlite3Err(E, pzErr);
sqlite3.free_(ppVTab);
result := SQLITE_ERROR;
exit;
end;
end;
struct := table.Structure;
result := sqlite3.declare_vtab(DB, pointer(struct));
if result <> SQLITE_OK then
begin
Notify('vt_Create(%) declare_vtab(%)', [modname, struct]);
table.Free;
sqlite3.free_(ppVTab);
result := SQLITE_ERROR;
end
else
ppVTab^.pInstance := table;
end;
function vt_Disconnect(pVTab: PSqlite3VTab): integer; cdecl;
begin
TOrmVirtualTable(pVTab^.pInstance).Free;
sqlite3.free_(pVTab);
result := SQLITE_OK;
end;
function vt_Destroy(pVTab: PSqlite3VTab): integer; cdecl;
begin
if TOrmVirtualTable(pVTab^.pInstance).Drop then
result := SQLITE_OK
else
begin
Notify('vt_Destroy', []);
result := SQLITE_ERROR;
end;
vt_Disconnect(pVTab); // release memory
end;
const
COST2DOUBLE: array[TOrmVirtualTablePreparedCost] of double = (
1E10, // costFullScan
1E8, // costScanWhere
10, // costSecondaryIndex
1); // costPrimaryIndex
function vt_BestIndex(var pVTab: TSqlite3VTab;
var pInfo: TSqlite3IndexInfo): integer; cdecl;
var
prepared: POrmVirtualTablePrepared;
table: TOrmVirtualTable;
i, n: PtrInt;
begin
result := SQLITE_ERROR;
table := TOrmVirtualTable(pVTab.pInstance);
if (cardinal(pInfo.nOrderBy) > MAX_SQLFIELDS) or
(cardinal(pInfo.nConstraint) > MAX_SQLFIELDS) then
begin
// avoid buffer overflow
Notify('nOrderBy=% nConstraint=%', [pInfo.nOrderBy, pInfo.nConstraint]);
exit;
end;
prepared := sqlite3.malloc(SizeOf(TOrmVirtualTablePrepared));
try
// encode the incoming parameters into prepared^ record
prepared^.WhereCount := pInfo.nConstraint;
prepared^.EstimatedCost := costFullScan;
for i := 0 to pInfo.nConstraint - 1 do
with prepared^.Where[i],
pInfo.aConstraint^[i] do
begin
OmitCheck := False;
Value.VType := ftUnknown;
if usable then
begin
Column := iColumn;
case op of
SQLITE_INDEX_CONSTRAINT_EQ:
Operation := soEqualTo;
SQLITE_INDEX_CONSTRAINT_GT:
Operation := soGreaterThan;
SQLITE_INDEX_CONSTRAINT_LE:
Operation := soLessThanOrEqualTo;
SQLITE_INDEX_CONSTRAINT_LT:
Operation := soLessThan;
SQLITE_INDEX_CONSTRAINT_GE:
Operation := soGreaterThanOrEqualTo;
SQLITE_INDEX_CONSTRAINT_MATCH:
Operation := soBeginWith;
else
Column := VIRTUAL_TABLE_IGNORE_COLUMN; // unhandled operator
end;
end
else
Column := VIRTUAL_TABLE_IGNORE_COLUMN;
end;
prepared^.OmitOrderBy := false;
if pInfo.nOrderBy > 0 then
begin
assert(SizeOf(TOrmVirtualTablePreparedOrderBy) = SizeOf(TSqlite3IndexOrderBy));
prepared^.OrderByCount := pInfo.nOrderBy;
MoveFast(pInfo.aOrderBy^[0], prepared^.OrderBy[0],
pInfo.nOrderBy * SizeOf(prepared^.OrderBy[0]));
end
else
prepared^.OrderByCount := 0;
// perform the index query
if not table.Prepare(prepared^) then
exit;
// update pInfo and store prepared into pInfo.idxStr for vt_Filter()
n := 0;
for i := 0 to pInfo.nConstraint - 1 do
if prepared^.Where[i].Value.VType <> ftUnknown then
begin
if i <> n then
// expression needed for Search() method to be moved at [n]
MoveFast(prepared^.Where[i], prepared^.Where[n],
SizeOf(prepared^.Where[i]));
inc(n);
pInfo.aConstraintUsage[i].argvIndex := n;
pInfo.aConstraintUsage[i].omit := prepared^.Where[i].OmitCheck;
end;
prepared^.WhereCount := n; // will match argc in vt_Filter()
if prepared^.OmitOrderBy then
pInfo.orderByConsumed := 1
else
pInfo.orderByConsumed := 0;
pInfo.estimatedCost := COST2DOUBLE[prepared^.EstimatedCost];
if sqlite3.VersionNumber >= 3008002000 then
// starting with SQLite 3.8.2: fill estimatedRows
case prepared^.EstimatedCost of
costFullScan:
pInfo.estimatedRows := prepared^.EstimatedRows;
costScanWhere:
// estimate a WHERE clause is a slight performance gain
pInfo.estimatedRows := prepared^.EstimatedRows shr 1;
costSecondaryIndex:
pInfo.estimatedRows := 10;
costPrimaryIndex:
pInfo.estimatedRows := 1;
else
EOrmException.RaiseUtf8(
'vt_BestIndex: unexpected EstimatedCost=%',
[ord(prepared^.EstimatedCost)]);
end;
pInfo.idxStr := pointer(prepared);
pInfo.needToFreeIdxStr := 1; // will do sqlite3.free(idxStr) when needed
result := SQLITE_OK;
{$ifdef OrmVirtualLOGS}
if table.Static is TRestStorageExternal then
TRestStorageExternal(table.Static).ComputeSql(prepared^);
SQLite3Log.Add.Log(sllDebug, 'vt_BestIndex(%) plan=% -> cost=% rows=%',
[sqlite3.VersionNumber, ord(prepared^.EstimatedCost),
pInfo.estimatedCost, pInfo.estimatedRows]);
{$endif OrmVirtualLOGS}
finally
if result <> SQLITE_OK then
// avoid memory leak on error
sqlite3.free_(prepared);
end;
end;
function vt_Filter(var pVtabCursor: TSqlite3VTabCursor; idxNum: integer;
const idxStr: PUtf8Char; argc: integer;
var argv: TSqlite3ValueArray): integer; cdecl;
var
prepared: POrmVirtualTablePrepared absolute idxStr; // idxNum is not used
i: PtrInt;
begin
result := SQLITE_ERROR;
if prepared^.WhereCount <> argc then
begin
// invalid prepared array (should not happen)
Notify('vt_Filter WhereCount=% argc=%', [prepared^.WhereCount, argc]);
exit;
end;
for i := 0 to argc - 1 do
SQlite3ValueToSqlVar(argv[i], prepared^.Where[i].Value);
if TOrmVirtualTableCursor(pVtabCursor.pInstance).Search(prepared^) then
result := SQLITE_OK
else
Notify('vt_Filter Search()', []);
end;
function vt_Open(var pVTab: TSqlite3VTab;
var ppCursor: PSqlite3VTabCursor): integer; cdecl;
var
table: TOrmVirtualTable;
begin
ppCursor := sqlite3.malloc(SizeOf(TSqlite3VTabCursor));
if ppCursor = nil then
begin
result := SQLITE_NOMEM;
exit;
end;
table := TOrmVirtualTable(pVTab.pInstance);
if (table = nil) or
(table.Module = nil) or
(table.Module.CursorClass = nil) then
begin
Notify('vt_Open', []);
sqlite3.free_(ppCursor);
result := SQLITE_ERROR;
exit;
end;
ppCursor.pInstance := table.Module.CursorClass.Create(table);
result := SQLITE_OK;
end;
function vt_Close(pVtabCursor: PSqlite3VTabCursor): integer; cdecl;
begin
TOrmVirtualTableCursor(pVtabCursor^.pInstance).Free;
sqlite3.free_(pVtabCursor);
result := SQLITE_OK;
end;
function vt_Next(var pVtabCursor: TSqlite3VTabCursor): integer; cdecl;
begin
if TOrmVirtualTableCursor(pVtabCursor.pInstance).Next then
result := SQLITE_OK
else
result := SQLITE_ERROR;
end;
function vt_Eof(var pVtabCursor: TSqlite3VTabCursor): integer; cdecl;
begin
if TOrmVirtualTableCursor(pVtabCursor.pInstance).HasData then
result := 0
else
result := 1; // reached actual EOF
end;
function vt_Column(var pVtabCursor: TSqlite3VTabCursor;
sContext: TSqlite3FunctionContext; N: integer): integer; cdecl;
var
res: TSqlVar;
begin
res.VType := ftUnknown;
if (N >= 0) and
TOrmVirtualTableCursor(pVtabCursor.pInstance).Column(N, res) and
SqlVarToSQlite3Context(res, sContext) then
result := SQLITE_OK
else
begin
Notify('vt_Column(%) res=%', [N, ord(res.VType)]);
result := SQLITE_ERROR;
end;
end;
function vt_Rowid(var pVtabCursor: TSqlite3VTabCursor;
var pRowid: Int64): integer; cdecl;
var
res: TSqlVar;
begin
result := SQLITE_ERROR;
if TOrmVirtualTableCursor(pVtabCursor.pInstance).Column(-1, res) then
begin
case res.VType of
ftInt64:
pRowid := res.VInt64;
ftDouble:
pRowid := trunc(res.VDouble);
ftCurrency:
pRowid := trunc(res.VCurrency);
ftUtf8:
pRowid := GetInt64(res.VText);
else
begin
Notify('vt_Rowid res=%', [ord(res.VType)]);
exit;
end;
end;
result := SQLITE_OK;
end
else
Notify('vt_Rowid Column', []);
end;
function vt_Update(var pVTab: TSqlite3VTab; nArg: integer;
var ppArg: TSqlite3ValueArray; var pRowid: Int64): integer; cdecl;
var
values: TSqlVarDynArray;
table: TOrmVirtualTable;
id0, id1: Int64;
i: PtrInt;
ok: boolean;
begin
// call Delete/Insert/Update methods according to supplied parameters
table := TOrmVirtualTable(pVTab.pInstance);
result := SQLITE_ERROR;
if (nArg <= 0) or
(nArg > 1024) then
exit;
case sqlite3.value_type(ppArg[0]) of
SQLITE_INTEGER:
id0 := sqlite3.value_int64(ppArg[0]);
SQLITE_NULL:
id0 := 0;
else
exit; // invalid call
end;
if nArg = 1 then
ok := table.Delete(id0)
else
begin
case sqlite3.value_type(ppArg[1]) of
SQLITE_INTEGER:
id1 := sqlite3.value_int64(ppArg[1]);
SQLITE_NULL:
id1 := 0;
else
exit; // invalid call
end;
SetLength(values, nArg - 2);
for i := 0 to nArg - 3 do
SQlite3ValueToSqlVar(ppArg[i + 2], values[i]);
if id0 = 0 then
ok := table.Insert(id1, values, pRowid)
else
ok := table.Update(id0, id1, values);
end;
if ok then
result := SQLITE_OK
else
Notify('vt_Update(%)', [pRowid]);
end;
function InternalTrans(pVTab: TSqlite3VTab; aState: TOrmVirtualTableTransaction;
aSavePoint: integer): integer;
begin
if TOrmVirtualTable(pVTab.pInstance).Transaction(aState, aSavePoint) then
result := SQLITE_OK
else
begin
Notify('Transaction(%,%)', [ToText(aState)^, aSavePoint]);
result := SQLITE_ERROR;
end;
end;
function vt_Begin(var pVTab: TSqlite3VTab): integer; cdecl;
begin
result := InternalTrans(pVTab, vttBegin, 0);
end;
function vt_Commit(var pVTab: TSqlite3VTab): integer; cdecl;
begin
result := InternalTrans(pVTab, vttCommit, 0);
end;
function vt_RollBack(var pVTab: TSqlite3VTab): integer; cdecl;
begin
result := InternalTrans(pVTab, vttRollBack, 0);
end;
function vt_Sync(var pVTab: TSqlite3VTab): integer; cdecl;
begin
result := InternalTrans(pVTab, vttSync, 0);
end;
function vt_SavePoint(var pVTab: TSqlite3VTab; iSavepoint: integer): integer; cdecl;
begin
result := InternalTrans(pVTab, vttSavePoint, iSavepoint);
end;
function vt_Release(var pVTab: TSqlite3VTab; iSavepoint: integer): integer; cdecl;
begin
result := InternalTrans(pVTab, vttRelease, iSavepoint);
end;
function vt_RollBackTo(var pVTab: TSqlite3VTab; iSavepoint: integer): integer; cdecl;
begin
result := InternalTrans(pVTab, vttRollBackTo, iSavepoint);
end;
function vt_Rename(var pVTab: TSqlite3VTab; const zNew: PUtf8Char): integer; cdecl;
begin
if TOrmVirtualTable(pVTab.pInstance).Rename(RawUtf8(zNew)) then
result := SQLITE_OK
else
begin
Notify('vt_Rename(%)', [zNew]);
result := SQLITE_ERROR;
end;
end;
procedure sqlite3InternalFreeModule(p: pointer); cdecl;
begin
if (p <> nil) and
(TOrmVirtualTableModuleSQLite3(p).fDB <> nil) then
TOrmVirtualTableModuleSQLite3(p).Free;
end;
{ TOrmVirtualTableModuleSQLite3 }
function TOrmVirtualTableModuleSQLite3.FileName(
const aTableName: RawUtf8): TFileName;
begin
if FilePath <> '' then
// if a file path is specified (e.g. by SynDBExplorer) -> always use this
result := inherited FileName(aTableName)
else if SameText(DB.FileName, SQLITE_MEMORY_DATABASE_NAME) then
// in-memory databases virtual tables should remain in memory
result := ''
else
// change file path to current DB folder
result := ExtractFilePath(DB.FileName) +
ExtractFileName(inherited FileName(aTableName));
end;
procedure TOrmVirtualTableModuleSQLite3.Attach(aDB: TSqlDataBase);
begin
if aDB = nil then
ERestStorage.RaiseUtf8('aDB=nil at %.SetDB()', [self]);
if fDB <> nil then
ERestStorage.RaiseUtf8('fDB<>nil at %.SetDB()', [self]);
FillCharFast(fModule, SizeOf(fModule), 0);
fModule.iVersion := 1;
fModule.xCreate := vt_Create;
fModule.xConnect := vt_Create;
fModule.xBestIndex := vt_BestIndex;
fModule.xDisconnect := vt_Disconnect;
fModule.xDestroy := vt_Destroy;
fModule.xOpen := vt_Open;
fModule.xClose := vt_Close;
fModule.xFilter := vt_Filter;
fModule.xNext := vt_Next;
fModule.xEof := vt_Eof;
fModule.xColumn := vt_Column;
fModule.xRowid := vt_Rowid;
if vtWrite in Features then
begin
fModule.xUpdate := vt_Update;
if vtTransaction in Features then
begin
fModule.xBegin := vt_Begin;