/includes/mORMot/SQLite3/mORMot.pas

https://bitbucket.org/zedxxx/sdb_util · Pascal · 57770 lines · 39494 code · 3399 blank · 14877 comment · 5462 complexity · be8ec350873b0c865e1a8de37c83c513 MD5 · raw file

  1. /// Common ORM and SOA classes for mORMot
  2. // - this unit is a part of the freeware Synopse mORMot framework,
  3. // licensed under a MPL/GPL/LGPL tri-license; version 1.18
  4. unit mORMot;
  5. (*
  6. This file is part of Synopse mORMot framework.
  7. Synopse mORMot framework. Copyright (C) 2016 Arnaud Bouchez
  8. Synopse Informatique - http://synopse.info
  9. *** BEGIN LICENSE BLOCK *****
  10. Version: MPL 1.1/GPL 2.0/LGPL 2.1
  11. The contents of this file are subject to the Mozilla Public License Version
  12. 1.1 (the "License"); you may not use this file except in compliance with
  13. the License. You may obtain a copy of the License at
  14. http://www.mozilla.org/MPL
  15. Software distributed under the License is distributed on an "AS IS" basis,
  16. WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  17. for the specific language governing rights and limitations under the License.
  18. The Original Code is Synopse mORMot framework.
  19. The Initial Developer of the Original Code is Arnaud Bouchez.
  20. Portions created by the Initial Developer are Copyright (C) 2016
  21. the Initial Developer. All Rights Reserved.
  22. Contributor(s):
  23. Alexander (chaa)
  24. Alfred Glaenzer (alf)
  25. Daniel Kuettner
  26. DigDiver
  27. EgorovAlex
  28. Emanuele (lele9)
  29. Esmond
  30. Goran Despalatovic (gigo)
  31. Jordi Tudela
  32. Maciej Izak (hnb)
  33. Martin Suer
  34. MilesYou
  35. Ondrej
  36. Pavel (mpv)
  37. Sabbiolina
  38. Vadim Orel
  39. Alternatively, the contents of this file may be used under the terms of
  40. either the GNU General Public License Version 2 or later (the "GPL"), or
  41. the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  42. in which case the provisions of the GPL or the LGPL are applicable instead
  43. of those above. If you wish to allow use of your version of this file only
  44. under the terms of either the GPL or the LGPL, and not to allow others to
  45. use your version of this file under the terms of the MPL, indicate your
  46. decision by deleting the provisions above and replace them with the notice
  47. and other provisions required by the GPL or the LGPL. If you do not delete
  48. the provisions above, a recipient may use your version of this file under
  49. the terms of any one of the MPL, the GPL or the LGPL.
  50. ***** END LICENSE BLOCK *****
  51. Client-Server ORM and SOA kernel of the mORMot framework
  52. **********************************************************
  53. - Client-Server classes using a RESTful model via JSON, over named pipes
  54. or Windows messages (HTTP/1.1 protocol with unit mORMotHttpServer/Client)
  55. - Client-Server ORM via TSQLRecord classes definition
  56. - Client-Server interface-based services for SOA process
  57. - optimized low-level RTTI and JSON process (TSQLTable)
  58. - ready to be used by mORMotSQLite3.pas with a SQLite3 engine
  59. - implements in-memory database tables with JSON/binary disk persistence
  60. Initial version: 2008 March, by Arnaud Bouchez
  61. Version 1.1 - 14 January 2010:
  62. - SQLite3 database layer updated to version 3.6.22
  63. - new communication layer, to be used on a local computer: windows messages
  64. (see TSQLRestClientURIMessage class). On a local machine, this is faster
  65. than named pipes and http, for small messages (quite as fast as direct
  66. access); but named pipes seems to be better for bigger messages
  67. - allow to get rid of our Enhanced Runtime Library dependency if not available
  68. (e.g. for FPC or on cross-platform, or on Delphi version newer than Delphi
  69. 2007): just define the ENHANCEDRTL conditional below if installed it
  70. (always undefined if UNICODE is defined)
  71. - attempt to reach Delphi 2009/2010 compilation (string=UnicodeString):
  72. the UNICODE conditional will adapt the framework to these compilers
  73. (you shouldn't have to change any conditional define below)
  74. - attempt to reach Free Pascal Compiler 2.4.0 compatibility
  75. - all asm code equivalence in pure pascal code for 64 bits compatibility
  76. (always slower, but always portable to all CPUs)
  77. - use of PtrUInt / PtrInt for 64 bits compatibility
  78. - in case of FPC, the typinfo.pp unit is used: so all published properties
  79. in any TSQLRecord descendant must have a setter (i.e. a "write fValue"
  80. statement); the FPC's RTTI is not the same as Delphi's: it's a shame :(
  81. Version 1.2 - 16 January 2010
  82. - compatibility tested up to Delphi 2009 (that is tested with Delphi 7,
  83. Delphi 2007 and Delphi 2009), with or without our Enhanced
  84. Run Time Library (which now compiles up to Delphi 2007) - by default, the
  85. ENHANCEDRTL compiler conditional is not defined;
  86. - lot of rewrite made for the Unicode version of the compiler (tested with
  87. Delphi 2009) - it was quite difficult to avoid most implicit conversions...
  88. the compiler is a bit tricky, as soon as you want to use the UTF-8 encoding
  89. (as we do, since JSON and HTTP do like this encoding, together as SQLite3),
  90. and not Unicode (which is in fact UTF-16);
  91. - can create an internal TSQLRestClientURIMessage messaging window, if
  92. you don't have any User Interface (so you don't have to add Forms unit)
  93. - named pipe speed improvement (10 times faster) under Vista and Seven
  94. Version 1.3 - January 22, 2010
  95. - disconnect piped name client after 30 min of inactivity to free the
  96. corresponding thread resource if the client failed to close cleanly
  97. - some small fixes (e.g. integer to and from string conversion, GPF) and
  98. multi-compiler (Delphi 2009/2010, especialy with TypInfo define) compatibility
  99. - compiler conditional renamed ENHANCEDRTL instead of ENHANCEDTRTL
  100. - new TSQLTable.GetRowValues overloaded methods
  101. Version 1.3.1 - January 23, 2010
  102. - some Pos() overload problem under Delphi 20007 compiler; since
  103. this Pos() is seldom used, it was decided to only define it under
  104. Delphi 2009/2010 (which expect such a RawUTF8 specific overloaded version)
  105. - source code adaptation to use only ASCII 7 bits characters: should be OK
  106. with asiatic MBCS text encoding and such IDE
  107. Version 1.3.2 - January 24, 2010
  108. - new StringToUTF8() and UTF8ToString() functions; it's prefered to use
  109. TLanguageFile.StringToUTF8() method from SQLite3i18n, which will handle full
  110. i18n of your application; these functions use the current RTL codepage
  111. under Delphi 3-2007
  112. Version 1.4 - February 8, 2010
  113. - whole Synopse SQLite3 database framework released under the GNU Lesser
  114. General Public License version 3, instead of generic "Public Domain"
  115. Version 1.5 - March 10, 2010
  116. - SQLite3 database layer updated to version 3.6.23
  117. - User Interface Query action implementation
  118. - added security attributes to the named pipes creation: now this
  119. communication layer can work as a windows service, under Vista or Seven
  120. (thanks to esmond comment in our blog for the tip)
  121. - added new FastCGI server (not 100% tested)
  122. - first attempt to add REST paging requests for 'GET ModelRoot/TableName',
  123. as expected by the YUI DataSource Request Syntax for data pagination:
  124. see http://developer.yahoo.com/yui/datatable/#data
  125. Version 1.6
  126. - SQLite3 database layer updated to version 3.6.23.1
  127. - most useful functions are now shared in a separate SynCommons unit
  128. - the framework is now licensed under a MPL/GPL/LGPL tri-license
  129. - obscure JSON parsing bug fixed (when a field content finished with '\')
  130. Version 1.7
  131. - alter table with newly added fields to a TSQLRecord (see GetSQLAddField()
  132. methods)
  133. - some compatibility fixes for Delphi 2009/2010
  134. - fixed bug: negative numbers were not updated when calling *.Update()
  135. Version 1.8
  136. - includes Unitary Testing class and functions
  137. - update engine to version 3.7.0
  138. - new direct methods to handle BLOB fields from clients or servers
  139. - new URI to GET or PUT BLOB fields: ModelRoot/TableName/TableID/BlobFieldName
  140. - fixed bug in TSQLTable.GetJSONValues: FirstRow parameter not used
  141. - TTextWriter class moved from SQLite3Commons to SynCommons
  142. - new ModelRoot/[TableName/TableID/]MethodName RESTful GET/PUT request
  143. (see TSQLRestServerCallBack definition and comments): implements some custom
  144. Client/Server request, similar to the DataSnap technology, but in a KISS way;
  145. it's fully integrated in the Client/Server architecture of our framework,
  146. and extend its ORM feature to Object-less communication - see new
  147. ModelRoot/Stat method implement in TSQLRestServer
  148. - floating point numbers are now encoded using new ExtendedStr[ing] methods:
  149. such values are now encoded in a more human readable way in the JSON content
  150. - new tests added (mostly relative to the new functions or classes)
  151. Version 1.9
  152. - update engine to version 3.7.1
  153. - fixed error calling a TSQLRestServerCallBack with both record & parameters
  154. - TSQLRecordSigned must now be signed with a specified content (content
  155. was formerly a fixed field of type RawUTF8, but it didn't apply in
  156. all cases, e.g. if content field is defined as TSQLRawBlob)
  157. - new TSQLRestClientURI.ForceBlobTransfert property which enable to
  158. get and set BLOB fields values with usual Add/Update/Retrieve methods
  159. - new TSQLRestClientURI.RetrieveBlobFields/UpdateBlobFields methods
  160. for retrieving/updating all BLOB fields of a record at once
  161. - better handling of sftID in the User Interface and database use (e.g.
  162. creates a dedicated index for the TSQLRecord published fields)
  163. - some code rewrite in order to avoid any implicit conversion from/to
  164. integer/cardinal after new definition of PtrInt/PtrUInt (matching
  165. NativeInt/NativeUInt types, available since Delphi 2007)
  166. - updated TSQLRibbonTabParameters object, with some new fields dedicated
  167. to the automatic edition of records, via the new SQLite3UIEdit unit
  168. - new sftSet SQL field kind, handling a TSQLRecord published property
  169. with a set of enumeration as Delphi type (stored as bit-mapped INTEGER)
  170. - handle now RowID as a valid alias to the ID field (needed for TSQLRecordFTS3)
  171. - defines a new TSQLRecordFTS3 type, for defining a FTS3 virtual table,
  172. i.e. implementing full-text search
  173. Version 1.9.1
  174. - update engine to version 3.7.2: an obscure but very old bug makes
  175. SQLite authors recommend to use 3.7.2 for all new development.
  176. Upgrading from all prior SQLite versions is also recommended.
  177. Version 1.9.2
  178. - WriteObject and CopyObject functions now handle Int64 properties,
  179. as TJSONWriter.WriteObject method does now also
  180. - new TSQLRestServerStatic.GetOne and TSQLRestServerStatic.UpdateOne methods,
  181. methods available since a TSQLRestServerStatic instance may be created
  182. stand-alone, i.e. without any associated Model/TSQLRestServer
  183. - diverse fixes in TSQLRestServerStatic which could occur in not expected
  184. behavior if security events are enabled for this table (wrong IDToIndex)
  185. - new TSQLRecordLog.CreateFrom method used to append some log records
  186. to an existing JSON log content
  187. Version 1.10
  188. - code modifications to compile with Delphi 6 compiler (Delphi 5 failed due
  189. to some obscure compiler bugs in SynCrypto.pas)
  190. - update SQLite3 engine to version 3.7.3
  191. Version 1.11
  192. - update SQLite3 engine to version 3.7.4
  193. - new TSQLRecordProperties class, used internally by TSQLRecord to access
  194. to the RTTI via some high-level structures (therefore save memory for each
  195. TSQLRecord instance, and make operations faster)
  196. - new TSQLRecordFill class, used internally by TSQLRecord.FillPrepare()
  197. to save memory: a TSQLRecord instance has now only 20 bytes of InstanceSize
  198. - TSQLRecord.ID reader has now a GetID() getter which can handle the fact that
  199. a published property declared as TSQLRecord (sftID type) contains not a
  200. true TSQLRecord instance, but the ID of this record: you can use
  201. aProperty.ID method in order to get the idea - but prefered method is to
  202. typecast it via PtrInt(aProperty), because GetID() relies on some low-level
  203. windows memory mapping trick
  204. - new TSQLRecordMany to handle "has many" and "has many through" relationships
  205. - TSQLRestServer.AfterDeleteForceCoherency now handles specifically
  206. TRecordReference, TSQLRecord (fixed) and new 'has many' Source/Dest fields
  207. (this is our internal "FOREIGN KEY" implementation - we choose not to
  208. rely on the database engine for that, in order to be engine-independent...
  209. and SQLite3 introduced FOREIGN KEY in 3.6.19 version only)
  210. - TSQLRestServer.AfterDeleteForceCoherency now synchronizes as expected
  211. TSQLRestServerStatic table content
  212. - new TSQLRestServerStatic.SearchField method, for fast retrieval of
  213. all IDs matching a field of a TSQLRestServerStatic table (faster than
  214. using any OneFieldValues method, which creates a temporary JSON content)
  215. - TSQLRecord.FillRow method has been made virtual, so that some calculated
  216. field can be initialize during table content enumeration
  217. - corrected possible GPF error in TSQLRestServer.Retrieve
  218. - sftMany/TSQLRecordMany field type handled as a not simple field
  219. - new TSQLRecord.SimplePropertiesFill() method, to fill the simple properties
  220. with a given list of const parameters, following the declared order of
  221. published properties of the supplied table
  222. - new TSQLRest.Add(aTable: TSQLRecordClass; const aSimpleFields: array of const)
  223. overloaded method to add a record from a supplied list of const parameters
  224. - new TSQLRest.Update(aTable,aID,aSimpleFields) overloaded method to update
  225. a record from a supplied list of const parameters for each simple field
  226. - new property TSQLRecord.SimpleFieldsCount
  227. - FTS3Match method renamed FTSMatch, in order to be used without hesitation
  228. for both FTS3 and FTS4 classes
  229. - new overloaded FTSMatch method, accepting ranking of MATCH, using the
  230. new RANK internal function - see http://www.sqlite.org/draft/fts3.html#appendix_a
  231. - new TSQLRecordFTS4 class, to handle new FTS4 extension module - see
  232. http://sqlite.org/fts3.html#section_1_1 - which is available since 3.7.4
  233. - new TSQLRecord.FillClose method
  234. - new TSQLRecord.CreateAndFillPrepare() methods, to makes loop into records
  235. easier (an hidden TSQLTable is created and released by TSQLRecord.Destroy)
  236. - new overloaded TSQLRestServer.CreateSQLIndex() method, accepting an array
  237. of field names
  238. - new TSQLRecord.FillPrepare(const aIDs: TIntegerDynArray) overloaded method,
  239. which can be handy to loop into some records via an IDs set
  240. - new TSQLTable.OwnerMustFree property for generic owning of a TSQLTable
  241. by a record - used for both CreateAndFillPrepare and TSQLRecordMany.FillMany
  242. - better non-ascii search handling in TSQLTable.SearchValue
  243. - source code modified to be 7 bit Ansi (so will work with all encodings)
  244. Version 1.12
  245. - now handle automaticaly prepared SQL statements: the parameters must
  246. be surrounded with :(...): in order to use an internal pool of prepared
  247. TSQLRequest statements; example of possible inlined values are :(1234):
  248. :(12.34): :(12E-34): :("text"): or :('text'): (with double quoting
  249. inside the text, just like any SQL statement)
  250. - with Delphi 2009+, you can define any string parameter in your
  251. TSQLRecord definition (will be handled as sftUTF8Text field)
  252. - with Delphi 2009+, WriteObject, ReadObject and CopyObject functions
  253. now handle string (UnicodeString) properties, as TINIWriter.WriteObject
  254. method does also: UTF-8 encoding is used at the storage level
  255. - new function SQLParamContent() to retrieve :(...): param content and type
  256. - another review of Pos() calls in the code (now use our fast PosEx)
  257. - some functions or type/const definitions moved to SynCommons in order
  258. to introduce new TSynTable class (TJSONWriter, IsRowID, GotoNextJSONField,
  259. TSynTableStatement...)
  260. - new TSQLRestServer.CreateSQLMultiIndex method
  261. - new TSQLTable.GetString and TSQLTable.GetVariant methods
  262. - new TPropInfo.SetVariant/GetVariant/CopyValue methods
  263. - new GetFieldValue/SetFieldValue and GetFieldVariant/SetFieldVariant
  264. methods for TSQLRecord
  265. - fixed issue in TSQLTable.GetWP(), which truncated data in Grid display
  266. - fixed issue in TSQLRestServerNamedPipe[Response] multi-thread architecture:
  267. FastMM in full debug mode detected that a block has been modified after
  268. being freed - now TSQLRestServerNamedPipeResponse is fully stand-alone
  269. Version 1.13
  270. - the ORM will now include all published properties of the parents, up to
  271. TSQLRecord, to the database fields (it was only using the published
  272. properties at the topmost class level)
  273. - dynamic arrays can now be specified for TSQLRecord published properties:
  274. a new sftBlobDynArray field kind has been added - will be stored as BLOB in
  275. the database (following the TDynArray.SaveTo binary stream layout), and
  276. will be transmitted as Base64 encoded in the JSON stream - we implemented
  277. a sftBlobRecord field kind, but Delphi RTTI is not generated for published
  278. properties of records: so our code is disabled (see PUBLISHRECORD
  279. conditional) :( - but you can use dynamic arrays of records
  280. - TPersistent can be now be specified for TSQLRecord published properties:
  281. a new sftObject field kind has been added - will be stored as TEXT in the
  282. database (following the ObjectToJSON serialization format) - TStrings or
  283. TRawUTF8List will be stored as a JSON array of string, and TCollection
  284. as a JSON array of objects, other TPersistent classes will have their
  285. published properties serialized as a JSON object
  286. - introducing direct content filtering and validation using
  287. TSynFilterOrValidate dedicated classes
  288. - filtering is handled directly in the new TSQLRecord.Filter virtual method,
  289. or via some TSynFilter classes - TSynFilterUpperCase, TSynFilterUpperCaseU,
  290. TSynFilterLowerCase, TSynFilterLowerCaseU and TSynFilterTrim e.g.
  291. - validation is handled in the new TSQLRecord.Validate virtual method,
  292. or via some TSynValidate classes - TSynValidateRest, TSynValidateIPAddress,
  293. TSynValidateEmail, TSynValidatePattern, TSynValidatePatternI,
  294. TSynValidateText, TSynValidatePassWord e.g.
  295. - introducing TSQLRecordRTree to implement R-Tree virtual tables - and new
  296. TSQLRecordVirtual parent table for all virtual tables like TSQLRecordFTS*
  297. - new TSQLRestClientURI methods to implement BATCH sequences to speed up
  298. database modifications: after a call to BatchStart, database modification
  299. statements are added to the sequence via BatchAdd/BatchUpdate/BatchDelete,
  300. then all statments are sent as once to the remote server via BatchSend -
  301. this is MUCH faster than individual calls to Add/Update/Delete in case
  302. of a slow remote connection (typically HTTP over Internet)
  303. - introducing TSQLVirtualTableModule / TSQLVirtualTable /
  304. TSQLVirtualTableCursor classes for a generic Virtual table mechanism
  305. (used e.g. by TSQLVirtualTableModuleDB in the SQLite3 unit)
  306. - new TSQLRecordVirtualTableAutoID and TSQLRecordVirtualTableForcedID
  307. classes, used to access any TSQLVirtualTable in our ORM
  308. - security and per-user access rights is now implemented in the framework
  309. core using per-User authentication via in-memory sessions (stored as
  310. TAuthSession), with group-defined associated security parameters (via
  311. TSQLAuthUser and TSQLAuthGroup tables), and RESTful Query Authentication
  312. via URI signature; should avoid most MITM and replay attacks
  313. - new TJSONSerializer class and ObjectToJSON/JSONToObject method
  314. (handles also dynamic arrays following the TTextWriter.AddDynArrayJSON
  315. format, i.e. plain JSON array for common types aka '[1,2,3]', but Base64
  316. encoded stream aka '["\uFFF0base64encodedbinary"]' for other arrays) and
  317. corresponding UrlDecodeObject() function (to be called by RESTful Services
  318. implementation on Server side)
  319. - wider usage of TSQLRecordProperties, for faster RTTI access, via the new
  320. class function TSQLRecord.RecordProps: TSQLRecordProperties: only
  321. virtual class function or procedure are now defined in TSQLRecord
  322. - enhanced TPropInfo.GetLongStrValue/SetLongStrValue methods, now converting
  323. RawUnicode, WinAnsiString, TSQLRawBlob and AnsiString properties
  324. - now ensure that no published property named ID or RowID was defined (this
  325. unique primary key field must be handled directly by TSQLRecord)
  326. - MAX_SQLFIELDS default is still 64, but can now be set to any value (64,
  327. 128, 192 and 256 have optimized fast code) so that you can have any number
  328. of fields in a Table
  329. - MAX_SQLTABLES default is now 256, i.e. you can have up to 256 tables in a
  330. TSQLModel instance (you can set any other value, on need)
  331. - removed MAX_SQLLOCKS constant non-sense (replaced by two dynamic arrays)
  332. - TSQLModel implementation speed up, in case of a huge number of registered
  333. TSQLRecord in the database Model (since MAX_SQLTABLES=256 by default)
  334. - enhanced TSQLRecordMany.DestGetJoinedTable method to handle custom fields
  335. - TSQLRecordMany.DestGetJoined* methods now accept generic TSQLRest class
  336. - new aCustomFieldsCSV parameter for FillPrepare / CreateAndFillPrepare
  337. methods of TSQLRecord, to retrieve only neeeded fields: be aware that
  338. not specified fields will be left untouched, so a later Update() call may
  339. corrupt the row data - this optional parameter is about to save bandwidth
  340. when retrieving records field in a loop
  341. - TSQLRestServerStaticInMemory can now store its content into UTF-8 JSON
  342. or an optimized (SynLZ) compressed binary format - associated TPropInfo
  343. GetBinary/SetBinary and TSQLRecord GetBinaryValues/SetBinaryValues methods
  344. - the generic TVarData type is now used as a standard way of fast values
  345. communication: only handled VType are varNull, varInt64, varDouble,
  346. varString (mapping a constant PUTF8Char), and varAny (BLOB with size =
  347. VLongs[0]) - used e.g. by SQLite3 unit (VarDataToContext/VarDataFromValue)
  348. - new TSQLRest.Retrieve(aPublishedRecord, aValue: TSQLRecord) and
  349. TSQLRecord.Create(aClient: TSQLRest; aPublishedRecord: TSQLRecord..) methods
  350. - ExecuteList defined in TSQLRest, so now available also in TSQLRestServer
  351. - added a magic pattern check to ignore broadcasted WM_COPYDATA message
  352. - fixed issue in serializing sftCurrency type in TSQLRecord.GetJSONValue
  353. Version 1.14
  354. - new TPropInfo.GetCurrencyValue method
  355. - fixed issue in produced JSON stream using '=' instead of ':'
  356. Version 1.15
  357. - unit now tested with Delphi XE2 (32 Bit)
  358. - new sftModTime / TModTime published field type in TSQLRecord, which will
  359. be set to the current server time stamp before update/adding
  360. - new sftCreateTime / TCreateTime published field type in TSQLRecord, which
  361. will be set to the current server time stamp at record creation
  362. - new TSQLRest.ServerTimeStamp property, which will return the current
  363. server time as TTimeLog/Int64 value (will use the new /TimeStamp RESTful
  364. service to retrieve the exact server time)
  365. - TSQLRestServerStaticInMemory uses a per-Table Critical Section to have
  366. its EngineList, EngineRetrieve, EngineAdd, EngineUpdate, EngineDelete,
  367. EngineRetrieveBlob, EngineUpdateBlob methods begin thread-safe
  368. - enhanced TSQLRestServer.URI thread-safety (e.g. Sessions access)
  369. - TSQLTable.InitFieldTypes will now also use column type retrieved during
  370. JSON parsing
  371. - new TSQLTable.GetCSVValues method
  372. - GetJSONValues() is now using an internal TRawByteStringStream when
  373. the expected result is a RawUTF8 (avoid copying content twice, and is
  374. perfectly thread-safe)
  375. - the shared fTempMemoryStream is not available any more (not thread-safe)
  376. - new TSQLRest.AcquireWrite/ReleaseWrite protected methods, used by
  377. TSQLRestServer.URI to safely write to the DB (e.g. for POST/PUT/DELETE...)
  378. with TSQLRest.AcquireWriteTimeOut, both thread-safe and transaction-safe
  379. - TSQLRest.TransactionBegin / Commit / RollBack methods now expect a
  380. SessionID parameter in order to allow safe concurent access: writing to
  381. the database is queued within a single client session
  382. - CreateSQLMultiIndex and CreateSQLIndex methods now working on external
  383. DB virtual tables (using SynDB.TSQLDBConnectionProperties.SQLAddIndex)
  384. - new TSQLRecordProperties.ExternalTableName and ExternalDatabase fields
  385. used by SQLite3DB to handle external SynDB-based database access
  386. - code refactoring to make TSQLRestServerStatic more generic (for SQLite3DB)
  387. - TSQLRestServer.UpdateField now accepts to search by ID or by value (used
  388. e.g. by rewritten TSQLRestServer.AfterDeleteForceCoherency method)
  389. - introducing TSQLRecordExternal kind of record, able to use any SynDB
  390. external database engine (e.g. OleDB/MSSQL/Oracle/MySQL/PostgreSQL/SQLite3)
  391. - new ExtractInlineParameters procedure to handle :(1234): SQL statements
  392. - new MakePrivateCopy property in TSQLTableJSON.Create, which will avoid
  393. creating a private copy of the JSON (used e.g. in SynDBExplorer to handle
  394. very large result sets, with half the memory)
  395. - new TSQLRecordProperties.SQLUpdateSet, SQLInsertSet and AppendFieldName
  396. properties/method (used for external DB handling)
  397. - new TSQLRecord.Create, TSQLRecord.FillPrepare,
  398. TSQLRecord.CreateAndFillPrepare, TSQLRest.OneFieldValue,
  399. TSQLRest.MultiFieldValues, TSQLRestClient.EngineExecuteFmt and
  400. TSQLRestClient.ListFmt overloaded methods, accepting both '%' and '?'
  401. characters in the SQL WHERE format text, inlining '?' parameters
  402. with :(...): and auto-quoting strings
  403. - new UnicodeComparison parameter in TSQLTable.SearchValue to handle
  404. property non WinAnsi (code page 1252) characters
  405. - fixed issue in TPropInfo.GetBinary method with dynamic arrays (used e.g.
  406. by TSQLRestServerStaticInMemory.SaveToBinary)
  407. - fixed issue with TAuthSession.IDCardinal=0 or 1 after 76 connections
  408. - fixed issue in SetInt64Prop() with a setter method
  409. - fixed potential issue in TSQLTable.SearchValue in case of invalid Client
  410. supplied parameter (now checks TSQLRest class type)
  411. Version 1.16
  412. - mORMot framework now implements Client-Server service implementation
  413. using regular Delphi interfaces (over a RESTful or JSON-RPC protocol),
  414. using TServiceContainer / TServiceFactory classes, accessible via
  415. TSQLRest.Services property, on both client and server side, with
  416. auto-marshaling, JSON serialization, and built-in security
  417. - added dedicated Exception classes (EORMException, EParsingException,
  418. ESecurityException, ECommunicationException, EBusinessLayerException,
  419. EServiceException) all inheriting from SynCommons.ESynException
  420. - added a generic JSON error message mechanism within the framework
  421. (including error code as integer and text, with custom error messages
  422. in RecordCanBeUpdated method and also in TSQLRestServerCallBackParams)
  423. - added TSQLRestServerFullMemory class to implement a basic REST server
  424. using only in-memory tables (will only handle CRUD commands, but is
  425. enough to handle authentication with optional persistence in JSON file)
  426. - added TSQLRestServerRemoteDB class to implement a REST server using a
  427. remote TSQLRestClient connection for all its ORM process: can be used
  428. e.g. to publish services with a dedicated process in a DMZ
  429. - deep refactoring of TSQLRestClient / TSQLRestClientURI methods, for
  430. better compliance with the Liskov substitution principle (LSP)
  431. - TSQLRestServer published method names are now hash-stored for speed
  432. - the TSQLRestServerCallBack method prototype has been modified to supply
  433. "var aParams: TSQLRestServerCallBackParams" as unique parameter:
  434. this is a CODE BREAK change and you shall refresh ALL your server-side
  435. code to match the new signature (using a record passed by value as
  436. parameter will ensure faster code and seamless evolution of this structure)
  437. - new TSQLRestServer.SessionGetUser method to be used e.g. by any
  438. TSQLRestServerCallBack method implementation to retrieve the connected user
  439. - now the TSQLAuthUser instance retrieved during the session opening will
  440. retrieve the Data BLOB field (ready to be consummed on the server side)
  441. - introduced TSQLRestServerSessionContext structure to include current User
  442. and Group ID to the execution context (in addition to the Session ID)
  443. - TSQLRestServerStaticInMemory binary format changed: now will store its
  444. content per field (to ensure better compression) - magic identifier changed
  445. - now TSQLRestClientURI BATCH sequences methods will allow to mix several
  446. TSQLRecord in its sequence of BatchAdd/BatchUpdate/BatchDelete calls - if
  447. initiated as BatchStart(nil)
  448. - code clean-up of TSQLRestClientURI.SetUser + added aHashedPassword optional
  449. parameter (to use already hashed password)
  450. - added TSQLOccasion to handle the special case of field type (like
  451. TCreateTime) in case of Upate/Insert/Select
  452. - TCreateTime published fields now are not modified at update
  453. - fixed unexpected exception raised in TSQLRecord.FillOne if FillPrepare
  454. was successfull, but did not return any row
  455. - introducing TSQLRest.Cache and TSQLRestCache class to handle Client or
  456. Server side fast in-memory cache (with tuned configuration and timeout)
  457. - associated TSQLRestServer.CacheFlush service for flushing the Server cache,
  458. and remote TSQLRestClientURI.ServerCacheFlush() method for the client
  459. - fixed issue in TSQLRecord.FillPrepare when the table has less columns
  460. that the filling TSQLTable (can occur e.g. when using aCustomFieldsCSV
  461. parameter in FillPrepare method)
  462. - EngineList methods (including TSQLRestServerStaticInMemory class) now
  463. handles an optional integer pointer, to return the count of row data
  464. - uses new generic TSynAnsiConvert classes for code page process: that is,
  465. SQLite3i18n S2U() and U2S() match the SynCommons StringToUTF8() and
  466. UTF8ToString() functions - therefore, the TUTF8ToStringEvent parameter is
  467. not useful any more
  468. - more than MAX_SQLFIELDS-1 columns (by default, 63) will raise an exception
  469. - added TJSONSerializer.RegisterCustomSerializer() method to allow JSON
  470. serialization of any class (thanks Pavel "aka mpv" for the idea and patch)
  471. - added TSQLRestServer.ServiceMethodByPassAuthentication method in order to
  472. allow by-pass of the RESTful authentication scheme for some methods (can
  473. be used e.g. to server some HTML content for a non SOA client)
  474. - fix issue about missing last item in JSONToObject() function
  475. - fix issue when handling null JSON objects in GetJSONObjectAsSQL() function
  476. - JSON functions now handle '0' as number according to http://json.org specs
  477. - fix issue about record locking in TSQLRestClientURI.Retrieve method
  478. - fix execution issue in TSQLRestServer.AfterDeleteForceCoherency()
  479. - fix issue about abusive session timeout: TSQLRestServer.SessionGet is now
  480. renamed SessionAccess and refreshes the session access timestamp each time
  481. a session is retrieved (+internal implementation fix)
  482. - fix issue in SetInt64Prop() procedure which failed the update of a property
  483. with no explicit setter
  484. - fix issue in TSQLRecord.FillFrom() which forgot to update InternalState
  485. - fix issue in TPropInfo.SetValue + TSQLRecord.ClearProperties with Value=nil
  486. - fix potential formating issue in TSQLTable.GetJSONValues/GetCSVValues
  487. methods which may create some wrong formating if TEXT is null/false/true
  488. (were formerly recognized as JSON keywords, whereas it should have already
  489. been transformed into nil, '0' or '1')
  490. - fix issue of unhandled buffer in TSQLTableJSON.UpdateFrom()
  491. - fix issue about transactions not working with TSQLRestServerStaticInMemory
  492. - fix issue in TSQLRestServerStaticInMemory on SELECT with only one column
  493. - fix TSQLTable.GetCSVValues() format (adding UTF-8 BOM)
  494. - TSQLRestServer.URI now returns "Location:" header without the digital
  495. signature (e.g. 'Location: People/11012') for a POST (=CRUD create/add)
  496. - TSQLRestClient.List and ListFmt methods now use TSQLRecordClass open
  497. array instead of TClass (for consistency)
  498. - new global RecordClassesToClasses() wrapper function to convert an
  499. array of TSQLRecordClass into the expected array of TClass
  500. - TPropInfo.CopyValue method now specifically handle copy of TCollection
  501. published properties items (used e.g. in TSQLRecord.FillFrom)
  502. - new GetEnumNameTrimed() wrapper function
  503. - new TRecordType definition, and TTypeinfo.RecordType associated method
  504. - now JSONToObject/ObjectToJSON functions and WriteObject method will handle
  505. standard TPersistent class serialization into/from JSON object
  506. - now ObjectToJSON/JSONToObject will unserialize sets and enumerations
  507. as an array of string, if HumanReadable is set to TRUE
  508. - now TSQLRestServer.Auth service returns true JSON response as specified
  509. by its content type (for better AJAX compatibility)
  510. - re-declared TSQLAccessRights record as an object, and added some
  511. dedicated methods: FromString, ToString, Edit
  512. - faster and more generic TSQLRecord.FillPrepare/FillRow implementation,
  513. including enhanced TSQLRecordFill class
  514. - faster TSQLRestServerStaticInMemory.LoadFromJSON and LoadFromBinary methods
  515. - reUrlEncodedSQL remote access right allows execution of SQL statement from
  516. a GET with the content encoded on the URI (as from XMLHTTPRequest)
  517. - new TSQLRest.EngineUpdateField protected method for a field content update
  518. (with PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=..)
  519. - new TSQLRecord.CreateAndFillPrepareMany and FillPrepareMany methods, able
  520. to create a JOINed SELECT statement including all nested TSQLRecordMany
  521. properties (including custom WHERE clause if necessary)
  522. - now nested TCollection and TStringList/TRawUTF8List objects are
  523. transmitted as true JSON arrays or objects for adding (POST) and update
  524. (PUT) - this will save bandwidth and increase compatibility
  525. with AJAX clients (they were formerly transmitted as JSON strings) - note
  526. that retrieval (GET) is not yet implemented, since it is faster to transmit
  527. directly the TEXT value as stored within the database
  528. - new TSQLRest.MainFieldIDs() method
  529. - new ForceID parameter for TSQLRest.Add() and TSQLRestClientURI.BatchAdd()
  530. to allow adding a record with a given ID
  531. - added TSQLRestClientURI.OnSetUser notification event (called from SetUser)
  532. - now TSQLRestClientURI.BatchUpdate() will set only ID, TModTime and mapped
  533. fields when called over a TSQLRecord on which a FillPrepare() was made
  534. (and no FillClose was performed)
  535. - now TSQLRestServerStats is a plain TPersistent class, and will be sent
  536. as a JSON object to the client
  537. - added function IsNotAjaxJSON() function - formerly internal IsExpanded()
  538. - added RecordManySourceProp / RecordManyDestProp / RecordManySourceClass /
  539. RecordManyDestClass to the TSQLRecordProperties
  540. - TSQLRestClientURI.CallBackPut() will now return any HTTP response content
  541. (even if it is not HTTP/1.1 compliant, and not work over some networks)
  542. - circumvent some bugs of Delphi XE2 background compiler (main compiler is OK)
  543. Version 1.17
  544. - TSQLRecord.Create/FillPrepare/CreateAndFillPrepare and
  545. TSQLRest.OneFieldValue/MultiFieldValues methods signature BREAKING CHANGE:
  546. array of const used to be ParamsSQLWhere and expecting '%' in the
  547. FormatSQLWhere statement, is now called BoundsSQLWhere, and expects bound
  548. parameters specified as '?' in the FormatSQLWhere statement - this is less
  549. confusing for new users, and more close to the usual way of preparing
  550. database queries; but your existing user code SHALL BE CHECKED and fixed
  551. - fixed issue in TSQLTable.GetJSONValues about JSON number encoding
  552. - added optional "rowCount": in TSQLRestServerStaticInMemory.GetJSONValues,
  553. TSQLTable.GetJSONValues and in TSQLTableJSON.ParseAndConvert for about
  554. 5% faster process of huge content (mpv proposal)
  555. - major speedup of TSQLTableJSON.Create(): buffer hashing by-passed until
  556. TSQLTableJSON.UpdateFrom() method is effectively called
  557. - fixed issue about BLOB unproperly serialized into JSON (e.g. now uses null)
  558. - fixed issue about harcoded 'ID' column, not compatible with virtual tables
  559. - fixed issue about pessimistic TSQLRestServerStaticInMemory.fIDSorted value
  560. - fixed ticket [fdf7158601] - about incorrect null value parsing in
  561. JSONToObject when isObj = oCustom (fix by mpv - thanks!)
  562. - fixed ticket [a1d9e9148e] - about incorrect reading empty JSON object by
  563. JSONToObject (fix by mpv - thanks!)
  564. - fixed ticket [4f5df7f18f] - about potential overflow of TSQLRestServerStats
  565. values (changed to QWord kind of property)
  566. - implemented feature request [7f6828999d] - about the possibility to use
  567. standard read/write in conjunction with custom read/write in JSONToObject
  568. and ObjectToJSON (mpv proposal - thanks!)
  569. - JSONToObject is now able to un-serialize a TObjectList class, when a class
  570. for its items is supplied as TObjectListItemClass optional parameter
  571. - ExtractInlineParameters() and SQLParamContent() decode ':("\uFFF12012-05-04"):'
  572. inlined parameters (i.e. text starting with JSON_SQLDATE_MAGIC after UTF-8
  573. encoding) as sftDateTime kind of parameter
  574. - added TJSONObjectDecoder.DecodedRowID member and fix GPF issue in Decode()
  575. - change vague boolean parameter into a TSQLOccasion enumerate in
  576. TJSONObjectDecoder.EncodeAsSQLPrepared()
  577. - added ForceID: boolean parameter to TSQLRest.Add() method
  578. - fixed random issue in TSQLRest.GetServerTimeStamp method (using wrongly
  579. TTimeLog direct arithmetic, therefore raising EncodeTime() errors)
  580. - internal cache added in TSQLRest.GetServerTimeStamp method for better speed
  581. - added TSQLRest.Retrieve() overloaded method for easy parameter binding
  582. - added TSQLRest.Delete() overloaded method with a WHERE clause parameter
  583. - implemented transaction process for (external database) virtual tables
  584. - added ReplaceRowIDWithID optional parameter to GetJSONObjectAsSQL(), in
  585. order to allow working with external DB not allowing RowID (e.g. Oracle)
  586. - TSQLRestCache.Notify*() methods made public for low-level potential use
  587. - made URI check case-insensitive (as for official RFC)
  588. - new TPropInfo.GetHash and TPropInfo.SameValue methods, with optional
  589. case sentivity handling
  590. - changed TSQLRecordProperties.BlobFieldsBits property into BlobFields,
  591. as an array of PPropInfo (for faster process)
  592. - added TSQLRecordProperties.HasTypeFields containing set of field types
  593. appearing in the record - replaces HasModTimeFields and HasCreateTimeField
  594. - new TListFieldHash class for efficient O(1) search using hashing handling
  595. - now unique fields are hashed in TSQLRestServerStaticInMemory implementation:
  596. "stored: false" properties are now checked for unicity before adding or
  597. update, and search will use the hash table for very fast O(1) process
  598. - speed optimization: all TSQLRestServerStaticInMemory search methods will
  599. now call a generic FindWhereEqual() for better code speed and maintenance
  600. - added ObjectFromInterface() function working also with TInterfacedObjectFake
  601. - introducing SetWeak() function to handle Weak interface assignment
  602. - added SetWeakZero() function and TObject class helper to handle ZEROed
  603. Weak interface assignment (with small performance penalty and memory use),
  604. corresponding to the ARC's Zeroing Weak pointers model
  605. - CopyObject() procedure now handle TCollection kind of object not only
  606. as sub properties
  607. - introducing TInterfacedCollection dedicated class, properly handling
  608. collection item creation on the Server side, with interface-based services:
  609. all contract operations shall use it instead of TCollection
  610. - changed the non expanded JSON format to use lowercase first column names:
  611. {"fieldCount":1,"values":["col1"... instead of {"FieldCount":1,"Values":[..
  612. - ensure root/table/id and root/table?select=...&where=... REST requests
  613. return plain standard JSON output for AJAX clients (not mORMot clients)
  614. - introducing TSQLRestServerURIContext.UserAgent and ClientKind properties
  615. - added TSQLTable.FieldLengthMax() and ExpandAsSynUnicode() methods
  616. - added BlobToBytes() function and TSQLTable.GetBytes/GetStream methods
  617. - added virtual TSQLRestServer.FlushInternalDBCache method and dedicated
  618. TSQLRestServerStaticInMemoryExternal class, to properly handle external
  619. DB modification for virtual tables (i.e. flush SQL/JSON cache as expected)
  620. - added virtual TSQLRestServer.BeginCurrentThread method
  621. - added virtual TSQLRestServer.EndCurrentThread method which will be called
  622. e.g. by TSQLite3HttpServer or TSQLRestServerNamedPipeResponse for each
  623. terminating threads, to release any thread-specific resources (like
  624. external DB connections defined in SQlite3DB)
  625. - added new TServiceMethod.ExecutionOptions member, and the new
  626. TServiceMethodExecutionOption[s] types - used by ExecuteInMainThread()
  627. - added TServiceFactoryServer.ExecuteInMainThread() method, to force a method
  628. to be executed with RunningThread.Synchronize() call on multi-thread server
  629. instances (e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse)
  630. - refactoring of TServiceMethod.InternalExecute low-level asm code, and
  631. changed the Instance parameter to be specified as an open array for
  632. fast execution over multiple instances of implementation classes
  633. - 'SELECT *' statements on virtual/external tables will by-pass the SQLite3
  634. virtual table module: TSQLRecord.FillPrepare can be up to 30% faster -
  635. added TSQLRestServerStatic.AdaptSQLForEngineList virtual method to
  636. handle most generic SELECT cases (overridden e.g. in SQLite3DB unit)
  637. - TSQLRestServerStaticInMemory.GetJSONValues will now generate expanded
  638. JSON content, if specified (only non-expanded format was implemented),
  639. via the new TSQLRestServerStaticInMemory.ExpandedJSON property
  640. - added TSQLRestServerStatic.InternalBatchStart / InternalBatchStop methods
  641. to handle fast grouped sending to remote database engine (e.g. Oracle
  642. bound arrays or MS SQL bulk insert via SynDB)
  643. - fixed issue in TSQLRestClientURI.EngineAdd() when server returned -1
  644. - changed TSQLRestServerCallBackParams content to be used as a generic
  645. parameters wrapper for both method callbacks and interface-based services:
  646. now aParams.Context.ID is to be used instead of aParams.ID
  647. - added TJSONObjectDecoder record/object helper for JSON object decoding
  648. (used e.g. by GetJSONObjectAsSQL() function, and for SQlite3DB process)
  649. - removed TSQLRecordExternal class type, to allow any TSQLRecord (e.g.
  650. TSQLRecordMany) to be used with VirtualTableExternalRegister() - there was
  651. indeed no implementation requirement to force a specific class type
  652. - added aUseBatchMode optional parameter to TSQLRecordMany.ManyDelete() method
  653. - now JSON parser will handle #1..' ' chars as whitespace (not only ' ')
  654. - now huge service JSON response is truncated (to default 20 KB) in logs
  655. Version 1.18
  656. - full Windows 64-bit compatibility, including RTTI and services
  657. - renamed SQLite3Commons.pas to mORMot.pas
  658. - BREAKING CHANGE: all ORM IDs are now declared as TID (=Int64) instead of
  659. integer - also added a new TIDDynArray type to be used e.g. for BatchSend,
  660. and declared the TRecordReference type as Int64 - whole API is impacted
  661. - BREAKING CHANGE in TSQLRestServerCallBackParams which is replaced by the
  662. TSQLRestServerURIContext class: in addition, all method-based services
  663. should be a procedure, and use Ctxt.Results()/Error() methods to return
  664. any content - new definition of Ctxt features now full access to
  665. incoming/outgoing context and parameters, especially via
  666. the new Input*[] properties, for easy URI parameter retrieval, and
  667. also allow define specific URI routing by a dedicated class
  668. - BREAKING CHANGE: TSQLRestServerStatic* classes are now renamed as
  669. TSQLRestStorage* and do not inherit from TSQLRestServer but plain TSQLRest
  670. for a much cleaner design, conform to the Liskov substitution principle
  671. - TSQLRestServer.StaticDataServer[] will now return an abstract TSQLRest
  672. - URI routing for interface-based service is now specified by the two
  673. TSQLRestRoutingREST and TSQLRestRoutingJSON_RPC classes (inheriting from
  674. the abstract TSQLRestServerURIContext class) instead of rmJSON and
  675. rmJSON_RPC enums - it allows any custom URI routing by inheritance
  676. - BREAKING CHANGE of TJSONWriter.WriteObject() method and ObjectToJSON()
  677. function: serialization is now defined with TTextWriterWriteObjectOptions
  678. set - therefore, TJSONSerializerCustomWriter callback signature changed
  679. - BREAKING CHANGE of TJSONSerializerCustomReader callback signature, which
  680. now has an additional aOptions: TJSONToObjectOptions parameter
  681. - BREAKING CHANGE with newly added reSQLSelectWithoutTable security policy
  682. flags in TSQLAccessRight.AllowRemoteExecute - older applications which
  683. expected any SELECT statement to be executed on the server may break:
  684. you need to explicitely set this flag for the User's TSQLAuthGroup - note
  685. that SELECT with a simple table name in its FROM clause will now be
  686. checked againsts TSQLAccessRight.GET[] access rights
  687. - BREAKING CHANGE: added aSentData parameter to TNotifySQLEvent/OnUpdateEvent
  688. - BREAKING CHANGE: SQL "where" clause defined as PUTF8Char constant text
  689. have been changed into RawUTF8, to let the compiler fully handle Unicode
  690. - BREAKING CHANGE: TSQLRecord.ID is a pure getter property - use the new
  691. IDValue read/write property to access the ID of a true TSQLRecord instance
  692. - remove some unused TPropInfo methods, which were duplicates of the
  693. TSQLPropInfo cleaner class hierarchy: SetValue/GetValue/GetValueVar
  694. GetBinary/SetBinary GetVariant/SetVariant NormalizeValue/SameValue GetHash
  695. IsSimpleField AppendName GetCaption GetSQLFromFieldValue SetFieldAddr
  696. - following the Liskov substitution principle, Execute/ExecuteFmt and
  697. protected EngineExecute() are defined for TSQLRest, replacing ExecuteAll()
  698. - TSQLRestServerRemoteDB will now redirect into any TSQLRest instance
  699. - you can now define any custom property and store it as JSON, e.g. TGUID,
  700. by using overriding InternalRegisterCustomProperties(), or directly as
  701. record published properties (since Delphi XE5) - see ticket [b653e5f4ca]
  702. - TSQLRestRoutingREST will now recognize several URI schemes:
  703. /root/Calculator.Add + body, /root/Calculator.Add?+%5B+1%2C2+%5D,
  704. even root/Calculator.Add?n1=1&n2=2 - and /root/Calculator/Add as a
  705. valid alternative to default /root/Calculator.Add, if needed
  706. - new TServiceMethodExecute class replacing TServiceMethod.InternalExecute:
  707. allows incoming parameters to be encoded as a JSON object, in
  708. addition to the standard JSON array - see request [48e30e0e05]
  709. - allow stubed/mocked interfaces to be exposed as SOA services
  710. - added optional CustomFields parameter to TSQLRest.Update() - and in case
  711. of a previous *FillPrepare() call, only the retrieved fields are updated
  712. - added TSQLRestServer.AcquireExecutionMode[] AcquireExecutionLockedTimeOut[]
  713. properties, able to define threading execution plan for ORM/SOA operations
  714. - added TSQLRestServer.InitializeTables() method to initialize void tables
  715. - changed RESTful URI to ModelRoot/Table?where=WhereClause to delete members
  716. - added TSQLRestServer.RootRedirectGet property to allow easy redirection
  717. - added TSQLRestServer.URIPagingParameters property, to support alternate
  718. URI parameters sets for request paging (in addition to YUI syntax),
  719. and an optional "total":... field within the JSON result (calling
  720. "SELECT count()" may be slow, especially on external databases)
  721. - added TSQLRest.PrivateGarbageCollector property, to manage lifetime
  722. of user class instances linked to a given TSQLRest
  723. - deep code refactoring, introducing TSQLPropInfo* classes in order to
  724. decouple the ORM definitions from the RTTI - will allow definition of
  725. any class members, even if there is no RTTI generated or via custom
  726. properties attributes or a fluent interface
  727. - new TJSONSerializer.RegisterClassForJSON() methods, allowing recognition
  728. of class types from a new {"ClassName":"TMyObject" JSON field generated
  729. by ObjectToJSON(..[woStoreClassName]) new option: it will be recognized
  730. by JSONToObject() for TObjectList members, and by the new JSONToNewObject()
  731. method - all TSQLRecord classes of a model are automaticaly registered
  732. - new TJSONSerializer.RegisterCollectionForJSON() method, to register a
  733. TCollection/TCollectionItem pair and allow JSON serialization of any
  734. "plain" collection - may be a good alternative to TInterfacedCollection
  735. - new JSONSerializer.RegisterObjArrayForJSON() method for automatic JSON
  736. serialization of T*ObjArray dynamic array storage
  737. - introducing ObjectEquals() global function for fast by value comparison
  738. - sets including all enumerate values will be written in JSON as "*"
  739. with woHumanReadable option (and recognized as such e.g. by JSONToObject);
  740. - new woStorePointer option to let ObjectToJSON() add "Address":"0431298a"
  741. - added ObjectFromInterfaceImplements() functions working with any
  742. implementation class, including TInterfacedObjectFake
  743. - introducing TInterfaceFactoryGenerated so that interface methods can be
  744. described for FPC, which lacks of expected RTTI - see [9357b49fe2]
  745. - introducing TInjectableObject to easily implement the DI/IoC SOLID
  746. patterns, for both TSQLRest services and stubing/mocking
  747. - introducing TInterfaceResolver, TInterfaceResolverForSingleInterface and
  748. TInterfaceResolverInjected, to be used for DI/IoC with TInjectableObject
  749. types and allowing TSQLRest.Services.Inject*() and Resolve() methods
  750. - added TSQLRest*.ServiceDefine() and enhanced TInterfaceStub/TInterfaceMock
  751. methods to specify interface from it name, without the need to use the
  752. TypeInfo(IMyInterface) syntax in end-user code
  753. - interface-based services are now able to work with TObjectList parameters
  754. - interface-based services will now avoid to transmit the "id":... value
  755. when ID equals 0
  756. - interface-based services can now return the result value as JSON object
  757. instead of JSON array if TServiceFactoryServer.ResultAsJSONObject is set
  758. (can be useful e.g. when consuming services from JavaScript)
  759. - interface-based services can now return the result value as XML object
  760. instead of JSON array or object if TServiceFactoryServer.ResultAsJSONObject
  761. is set (can be useful e.g. when consuming services from XML only clients) -
  762. as an alternative, ResultAsXMLObjectIfAcceptOnlyXML option will recognize
  763. 'Accept: application/xml' or 'Accept: text/xml' HTTP header and return
  764. XML content instead of JSON - with optional ResultAsXMLObjectNameSpace
  765. - added TServiceCustomAnswer.Status member to override default HTML_SUCCESS
  766. - new TSQLRest.Service<T: IInterface> method to retrieve a service instance
  767. - added TServiceMethodArgument.AddJSON/AddValueJSON/AddDefaultJSON methods
  768. - method-based services are now able to handle "304 Not Modified" optimized
  769. response to save bandwidth, in TSQLRestServerURIContext.Returns/Results
  770. - added TSQLRestServerURIContext.ReturnFile() and ReturnFileFromFolder()
  771. methods, for direct fast transmission to a HTTP client, handling
  772. "304 Not Modified" and proper mime type recognition
  773. - added TSQLRestServerURIContext.Input*OrVoid[] properties
  774. - added TSQLRestServerURIContext.SessionRemoteIP, SessionConnectionID,
  775. SessionUserName and ResourceFileName properties
  776. - added TSQLRestServerURIContext.InputAsMultiPart() method
  777. - added TSQLRestServerURIContext.Redirect() method for HTTP 301 commands
  778. - added TSQLRestServer.ServiceMethodRegister() low-level method
  779. - added TSQLRestServer.ServiceMethodRegisterPublishedMethods() to allow
  780. multi-class method-based services (e.g. for implementing MVC model)
  781. - new TSQLRestClientURI.ServicePublishOwnInterfaces/ServiceRetrieveAssociated
  782. methods, implementing a P2P automatic registration of associated services
  783. - ServiceContext threadvar will now be set in all ORM and SOA process, to
  784. allow access to the execution context
  785. - to make the implicit explicit, TSQLRestServerURIContext.ID has been
  786. renamed TableID, and a new ServiceInstanceID instance has been added
  787. - BeginCurrentThread/EndCurrentThread will now be defined at TSQLRest class
  788. abstract level, and code review has been made to ensure that they will
  789. be triggered as expected (i.e. always and from the thread itself)
  790. - new function CurrentServiceContext, to be used from packages instead of
  791. direct ServiceContext threadvar access - circumvent Delphi RTL/compiler
  792. restriction (bug?) as reported by [155b09dc1b]
  793. - let the ORM reading methods follow the SELECT column order using
  794. TSQLFieldIndexDynArray instead of TSQLFieldBits as expected by [94ff704bb1]
  795. - let TSQLRest.OneFieldValues() handle directly naive expressions like
  796. 'SELECT ID from Table where ID=10' or 'where ID in (10,20,30)'
  797. - new TSQLRestClientURI.ForceBlobTransfertTable[] property which enable to
  798. get and set BLOB fields values with usual Add/Update/Retrieve methods for
  799. a particular table (more tuned than existing ForceBlobTransfert property)
  800. - once authenticated, TSQLRestClientURI.SessionUser would have all its
  801. properties retrieved from the remote server
  802. - added TSQLRestClientURI.SessionID/SessionServer/SessionVersion properties
  803. - added TSQLRestClientURI.CallBack() method allowing any HTTP verb
  804. - added new TSQLRestClientURI.RetryOnceOnTimeout property
  805. - fixed TServiceFactoryClient.Get() not working properly in sicPerSession,
  806. sicPerUser or sicPerGroup modes - ticket [3fafb53be4]
  807. - added TServiceInstanceImplementation.sicPerThread mode - feature [cb76c866bb]
  808. - introduced more readable "stored AS_UNIQUE" published property definition
  809. in TSQLRecord (via the explicit AS_UNIQUE=false constant)
  810. - introduced TSQLRecord.Create(aSimpleFields) constructor
  811. - introduced 32bit/64bit safe TSQLRecord.AsTSQLRecord property, to be used
  812. when assigning IDs to a TSQLRecord published property
  813. - TSQLRecord.[CreateAnd]FillPrepare() will now handle aCustomFieldsCSV='*'
  814. parameter as a all fields selection, including BLOBs (whereas default ''
  815. value will continue to return simple fields, excluding BLOBs)
  816. - TSQLRecord.CreateAndFillPrepareMany() will raise an exception when run
  817. on a TSQLRecord with no many-to-many published field
  818. - introducing new TSQLRecord.EnginePrepareMany() method
  819. - added optional FieldBits output parameter to TSQLRecord.FillFrom/FillValue
  820. - fixed TSQLRecordMany Source/Dest fields serialization - see [22ce911c715]
  821. - introducing TSQLRecord.CreateJoined() and CreateAndFillPrepareJoined()
  822. constructors, to auto-initialize and load nested TSQLRecord properties
  823. - added TSQLRecord.GetAsDocVariant/GetSimpleFieldsAsDocVariant methods
  824. - added TSQLRecord.AppendAsJsonObject/AppendFillAsJsonArray and
  825. TSQLRest.AppendListAsJsonArray methods
  826. - TSQLRecord.InitializeTable() will now create DB indexes for aUnique
  827. fields (including ID/RowID)
  828. - TSQLRecord.CreateCopy will handle TStrings property via new CopyStrings()
  829. - added TSQLInitializeTableOptions parameter to CreateMissingTables and
  830. InitializeTable methods, to tune underlying table creation (e.g. indexes)
  831. - introducing TInterfaceStub and TInterfaceMock classes to define
  832. high-performance interface stubbing and mocking via a fluent interface
  833. - integrated Windows Authentication to the mORMot Client-Server layer: in
  834. order to enable it, define a SSPIAUTH conditional and call
  835. TSQLRestClientURI.SetUser() with an empty user name, and ensure that
  836. TSQLAuthUser.LoginName contains a matching 'DomainName\UserName' value
  837. - introducing TSQLRestServerAuthenticationActiveDirectory class, thanks to
  838. an implementation proposal from EgorovAlex - thanks for sharing!
  839. - added TSQLRecordTimed class, and TSQLRecord.AddFilterNotVoidAllTextFields
  840. and TSQLModel.AddTableInherited methods
  841. - new TSQLModel/TSQLRecordProperties.SetVariantFieldsDocVariantOptions methods
  842. - Windows Authentication can use either NTLM or the more secure Kerberos
  843. protocol, if the corresponding SPN domain is set as password
  844. - feature request [5a17a4277f]: you can now define in the Model your custom
  845. TSQLAuthUser and/or TSQLAuthGroup classes to store the authorization
  846. information: TSQLRestServer will search for any table inheriting from
  847. TSQLAuthUser/TSQLAuthGroup in the TSQLModel - see also corresponding
  848. TSQLRestServer.SQLAuthUserClass/SQLAuthGroupClass new properties, and
  849. the new generic TSQLRestServer.OnAuthenticationUserRetrieve optional event
  850. - introducing TSQLAuthUser.CanUserLog() to ensure authentication is allowed,
  851. as requested by feature request [842906425928]
  852. - added TSynAuthenticationRest e.g. for SynDBRemote to check REST users
  853. - added TSQLRestServer.OnSessionCreate/OnSessionClosed/OnAuthenticationFailed
  854. callbacks, and TSQLRestServerURIContext.AuthenticationFailed virtual method
  855. - added TSQLRestServer.SessionClass property to specify the class type
  856. to handle in-memory sessions, and override e.g. IsValidURI() method
  857. - CreateMissingTables() method is now declared as virtual in TSQLRestServer
  858. - TSQLRestServer.URI() and TSQLRestClientURI.InternalURI() methods now uses
  859. one TSQLRestURIParams parameter for all request input and output values
  860. - TSQLRestServer.URI() method will return "405 Method Not Allowed" error
  861. (HTML_NOTALLOWED) if the supplied URI does not match RestAccessRights
  862. - TSQLRestServer.URI() will now handle POST/PUT/DELETE ModelRoot/MethodName
  863. as method-based services
  864. - added TSQLRestServerFullMemory.Flush method-based service
  865. - added TSQLRestServerFullMemory.DropDatabase method
  866. - TSQLRestServerFullMemory now generates its expected InternalState value
  867. - completed HTML_* constant list and messages - feature request [d8de3eb76a]
  868. - handle HTML_NOTMODIFIED and HTML_TEMPORARYREDIRECT as successful status -
  869. as expected by feature request [5d2634e8a3]
  870. - enhanced sllAuth session creation/deletion logged information
  871. - introducing TSQLRest.LogClass property, allowing to set a custom log class
  872. - added TAuthSession.SentHeaders, RemoteIP and ConnectionID properties
  873. - added process of Variant and WideString types in TSQLRecord properties,
  874. including any custom type, like TDocVariant or TBSONVariant (for MongoDB
  875. objects), or even a dynamic array of variants (see [d9d091baab])
  876. - added JSON serialization of Variant and WideString types, and corresponding
  877. TJSONToObjectOptions optional parameter in JSONToObject() / ObjectToJSON()
  878. functions and WriteObject() method - including TDocVariant or TBSONVariant
  879. - fixed TPersistent process in TJSONWriter.WriteObject - thanks Jordi!
  880. - introducing TSynAutoCreateFields, TPersistentAutoCreateFields and
  881. TCollectionItemAutoCreateFields classes, with automatic initialization and
  882. finalization of their nested published properties (e.g. for DDD objects)
  883. - JSONToObject() is now able to unserialize a nested record - see [5e49b3096a]
  884. - added TTypeInfo.ClassCreate() method to create a TObject instance from RTTI
  885. - TEnumType.GetEnumNameValue() will now recognize both 'sllWarning' and
  886. 'Warning' text as a sllWarning item (will enhance JSONToObject() process)
  887. - fix and enhance boolean values parsing from JSON content ("Yes"=true)
  888. - implement woHumanReadableFullSetsAsStar and woHumanReadableEnumSetAsComment
  889. option for JSON serialization and TEnumType.GetEnumNameTrimedAll()
  890. - fixed ticket [139a846ce88] about TJSONObjectDecoder.EncodeAsSQLPrepared()
  891. - use GetTickCount64() to fix any issue in case of GetTickCount() overflow -
  892. some *: cardinal properties are renamed *64: Int64 for consistency
  893. - added ClassInstanceCreate() function calling any known virtual constructor
  894. - added GetInterfaceFromEntry() function to speed up interface execution,
  895. e.g. for TServiceFactoryServer (avoid the RTTI lookup of GetInterface)
  896. - added TPropInfo.ClassFromJSON() to properly unserialize TObject properties
  897. - added TPropInfo.CopyToNewObject() method, to instantiate class published
  898. properties from another instance (possibly one of its nested items)
  899. - added TSQLPropInfo.SQLFieldTypeName and SQLDBFieldTypeName properties
  900. - introducing TSQLPropInfo.SetValueVar() method to avoid a call to StrLen()
  901. - TSQLPropInfo is now able to "flatten" nested properties, e.g. DDD's
  902. TUser.Address.Country.Iso will be mapped to ORM's TSQLRecord.Address_Country
  903. - introducing TSQLPropInfo.CopyProp() method which supports flattened classes
  904. - fixed [f96cf0fc5d] and [221ee9c767] about TSQLRecordMany JSON serialization
  905. - fixed issue when retrieving a TSQLRecord containing TSQLRecordMany
  906. properties with external tables (like 'no such column DestList' error)
  907. via SQLite3 virtual tables (e.g. for a JOINed query like FillPrepareMany)
  908. - fixed TInterfacedCollection.GetClass to be defined as a class function
  909. - TSQLRestClientURINamedPipe and TSQLRestClientURIMessage are now thread-safe
  910. (i.e. protected by a system mutex) therefore can be called from a
  911. multi-threaded process, e.g. TSQLRestServer instances as proxies
  912. - modified named pipe client side to avoid unexpected file not found errors
  913. - TInterfaceFactory instances are now shared among all interface-based
  914. features (e.g. services, callbacks or mocks/stubs), in a thread-safe cache
  915. - added dedicated EInterfaceFactoryException
  916. - added TServiceFactoryServer.TimeoutSec / SetTimeoutSec() property / method
  917. - TServiceFactoryServer.ExecuteInMainThread() method is now replaced by
  918. a more generic TServiceFactoryServer.SetOptions() method
  919. - new optFreeInMainThread execution options for the service, allowing server
  920. side service class instance release via Synchronize() - ticket [57bea48f30]
  921. - new optExecInPerInterfaceThread and optFreeInPerInterfaceThread options
  922. for the service, allowing server side service class instance execution and
  923. release in a thread dedicated to the interface - ticket [8307f8a547]
  924. - new optExecLockedPerInterface option for the service, allowing server side
  925. service instance execution and release to be locked for the whole interface
  926. - added TServiceFactoryServer.ByPassAuthentication property to release
  927. authentication for a given interface-based service
  928. - stub creation speed-up by using a shared PAGE_EXECUTE_READWRITE buffer
  929. - added TServiceMethod.DefaultResult property, to be used for stubs/mocks
  930. - TServiceFactory.Create() and its children will now always have an optional
  931. aContractExpected parameter (for consistency with TServiceFactoryClient)
  932. - introduce smvVariant kind of parameter for interface-based services
  933. - new RawJSON string type to force no JSON serialization in interface-based
  934. services (to be used e.g. for transmitting TSQLTableJSON results)
  935. - safer TInterfacedObjectFake.FakeCall() stack use
  936. - TServiceFactoryServerInstance will now create instances server-side
  937. with a RefCount=1, to allow passing self as an interface in sub-methods
  938. - huge code refactoring of the ORM model implementation: a new dedicated
  939. TSQLModelRecordProperties will contain per-TSQLModel parameters, whereas
  940. shared information retrieved by RTTI remain in TSQLRecordProperties - this
  941. will allow use of the same TSQLRecord in several TSQLModel at once, with
  942. dedicated SQL auto-generation and external DB settings
  943. - added aExternalTableName/Database optional parameters to method
  944. TSQLModel.VirtualTableRegister()
  945. - added Owner, Actions, Events parameters to TSQLModel.Create() constructor
  946. - fixed issue in TSQLRestServer.Create() about authentication enabling
  947. - introducing TSQLRestServer.CreateWithOwnModel() constructor to ease
  948. creation of simple Rest in-memory storage, e.g. for testing purposes
  949. - added TSQLModel.GetTableIndexExisting() method to raise an explicit
  950. EModelException if the table is not part of the model - used now by
  951. almost all CRUD Client and Server operations - ticket [aa0d6f1e90]
  952. - added TSQLModel.URIMatch() method to allow sub-domains generic matching
  953. at database model level (so that you can set root='/root/sub1' URIs)
  954. - moved SQLFromSelectWhere() from a global function to a TSQLModel method
  955. (to prepare "Table per class hierarchy" mapping in mORMot)
  956. - SQLParamContent() / ExtractInlineParameters() functions moved to SynCommons
  957. - added TSQLRecordHistory and TSQLRestServer.TrackChanges() for [a78ffe992b]
  958. - added TSQLRestTempStorage "asynchronous write" for [cac2e379f0]
  959. - added TSQLRestServer.RecordVersionSynchronize() and the new TRecordVersion
  960. field kind to maintain a remote versioning of rows - see [3453f314d9]
  961. - TSQLAuthUser and TSQLAuthGroup have now "index ..." attributes to their
  962. RawUTF8 properties, to allow direct handling in external databases
  963. - added TSQLModelRecordProperties.FTS4WithoutContent() method to allow
  964. TSQLRecordFTS4 tables let the content be store in another TSQLRecord table
  965. - introducing TSQLRecordFTS3Unicode61 and TSQLRecordFTS4Unicode61 classes
  966. - new protected TSQLRestServer.InternalAdaptSQL method, extracted from URI()
  967. process to also be called by TSQLRestServer.MultiFieldValues() for proper
  968. TSQLRestStorage.AdaptSQLForEngineList(SQL) call
  969. - new TSQLRestStorage.fOutInternalStateForcedRefresh protected field to
  970. optionally force the refresh of the content
  971. - added TSQLRestStorageRemote class and TSQLRestServer.RemoteDataCreate()
  972. method for feature request [3453f314d97d]
  973. - new TSQLRestServer.OnBlobUpdateEvent: TNotifyFieldSQLEvent event handler
  974. to implement feature request [4cafc41f67]
  975. - new protected TSQLRestServer.InternalUpdateEvent virtual method, to allow
  976. a server-wide update notification, not coupled to OnUpdateEvent callback -
  977. see feature request [5688e97251]
  978. - TSQLRestStorageInMemory.AdaptSQLForEngineList() will now handle
  979. 'select count() from TableName' statements directly, and any RESTful
  980. requests from client
  981. - TSQLRestStorageInMemory will now handle SELECT .... WHERE ID IN (...)
  982. - fixed issue in TSQLRestStorageInMemory.EngineList() when only ID
  983. - added TSQLRestServerFullMemory.Storage[] and Storages[] properties
  984. - changed TSQLAccessRights and TSQLAuthGroup.SQLAccessRights CSV format
  985. to use 'first-last,' pattern to regroup set bits (reduce storage size)
  986. - added overloaded TSQLAccessRights.Edit() method using TSQLOccasions set
  987. - added reOneSessionPerUser flag to TSQLAccessRight.AllowRemoteExecute
  988. - added reUserCanChangeOwnPassword flag to TSQLAccessRight.AllowRemoteExecute
  989. as requested by [e6f113fc98]
  990. - enabled reUrlEncodedSQL by default for TSQLAccessRights (needed e.g. for
  991. plain HTTP GET request, without any body)
  992. - introducing TSQLRestClientURI.InternalCheckOpen/InternalClose methods to
  993. properly handle remote connection and re-connection
  994. - added TSQLRestClientURI.LastErrorCode/LastErrorMessage/LastErrorException
  995. properties, to retrieve additional information about remote URI() execution
  996. - added TSQLRestClientURI.ServiceRegister() and ServiceRegisterClientDriven()
  997. methods for easier Client-side interface-based services initialization
  998. - added JSONFileToObject() and ObjectToJSONFile() functions
  999. - unit interface deep refactoring: e.g. now TSQLTable will refers explicitly
  1000. to TSQLRecord classes and not to plain TClass (e.g. for QueryTables[])
  1001. - introducing new TSQLTable[JSON].CreateFromTables/CreateWithColumnTypes()
  1002. constructors, able to specify the column type information to be used
  1003. - added TSQLTable.SetFieldType() method to specify a column type and size
  1004. - introduced TSQLTable.FieldTypeIntegerDetectionOnAllRows property to force
  1005. the detection of number types for all data rows, if needed
  1006. - added TSQLTable.SortFields() overloaded method, able to sort a TSQLTable
  1007. row content by multiple fields - implements feature request [d277153f03]
  1008. - added optional CustomFormat: string parameter to TSQLTable.ExpandAsString()
  1009. to allow numerical or date/time format for a given column [749dfbdb6a]
  1010. - added optional CustomCompare: TUTF8Compare param to TSQLTable.SortFields()
  1011. to allow any kind of custom ordering - feature request [c6804d48a4]
  1012. - speed up of TSQLTable.FieldIndex() TSQLTable.FieldIndexExisting() methods
  1013. (using binary search)
  1014. - added overloaded TSQLTable.FieldIndex() and TSQLTable.FieldIndexExisting()
  1015. methods, to set several local field index integer variables at once
  1016. - added TSQLTable.ToObjectList() and ToObjectList<T: TSQLRecord>() methods
  1017. - added TSQLTable.Step() FieldBuffer() Field() FieldAsInteger() FieldAsFloat()
  1018. methods, handling a cursor at TSQLTable/TSQLTableJSON level, with optional
  1019. late-binding column access
  1020. - added TSQLTable.GetSynUnicode() method
  1021. - added TSQLTable.ToDocVariant() and TSQLRest.RetrieveDocVariantArray()
  1022. overloaded methods, which can be used e.g. to process directly some data
  1023. retrieved from the ORM with TSynMustache.Render()
  1024. - added TSQLTable.GetMSRowSetValues() methods, to return XML content in
  1025. ADODB.recordset format - thanks mpv and Vadim Orel for the input!
  1026. - added TSQLTable.GetODSDocument method, to return a document readable by
  1027. Office applications - thanks esmond for the idea and patch
  1028. - fixed ticket [5a8ec14e25] about potential GPF in TSQLTable.DeleteColumnValues
  1029. - added TSQLRecord.CreateAndFillPrepare(aJSON) overloaded method
  1030. - introducing TSQLRecordInterfaced class, if your TSQLRecord definition
  1031. should be able to implement interfaces
  1032. - in addition to Batch*() methods available at TSQLRestClientURI level, all
  1033. BATCH process is now implemented by stand-alone TSQLRestBatch instances,
  1034. which can safely be used at TSQLRestServer level, even from multi thread
  1035. - introduced "SIMPLE": and "SIMPLE@": commands in the JSON stream for
  1036. default BatchAdd() with simple fields (to reduce bandwidth and memory use)
  1037. - fixed BATCH process to generate valid JSON content
  1038. - fixed BATCH process to check for the TSQLAccessRights of the current
  1039. logged user just like other CRUD methods, as reported by [27cf02be50]
  1040. - ensure BATCH process take place within execORMWrite context [c47b9ef5800]
  1041. - added optional CustomFields parameter to TSQLRest.BatchUpdate()
  1042. and BatchAdd() methods - TModTime fields will always be sent
  1043. - implemented automatic transaction generation during BATCH process via
  1044. a new AutomaticTransactionPerRow parameter in BatchStart()
  1045. - fixed unexpected issue in TSQLRest.BatchSend() when nothing is to be sent
  1046. - added TSQLRestClientURI.ServerTimeStampSynchronize method to force time
  1047. synchronization with the server - can be handy to test the connection
  1048. - added TSQLRestClientURI.ServerRemoteLog wrapper to method-based service,
  1049. and corresponding ServerRemoteLogStart and ServerRemoteLogStop methods
  1050. - added TSQLRest.TableHasRows/TableRowCount methods, and overridden direct
  1051. implementation for TSQLRestServer/TSQLRestStorageInMemory (including
  1052. SQL pattern recognition for TSQLRestStorageInMemory)
  1053. - added TSQLRest.RetrieveList method to retrieve a TObjectList of TSQLRecord
  1054. - added TSQLRest.RetrieveList<T> generic method to retrieve a TObjectList<T>
  1055. - added TSQLRest.RetrieveListJSON method to get a TSQLRecord list as JSON
  1056. - added TSQLRest.RetrieveListObjArray and TSQLTable.ToObjArray methods
  1057. - added TSQLRest.UpdateField() overloaded methods to update a single field
  1058. - added TSQLRest.UpdateFieldIncrement() method for atomic increase/decrease
  1059. - "rowCount": is added in TSQLRestStorageInMemory.GetJSONValues,
  1060. TSQLTable.GetJSONValues and in TSQLTableJSON.ParseAndConvert, at the end
  1061. of the non expanded JSON content, if needed - improves client performance
  1062. - UpdateBlobFields() and RetrieveBlobFields() methods are now defined at
  1063. TSQLRest level, with dedicated implementation for TSQLRestClient* and
  1064. TSQLRestServer* classes - implements feature request [34664934a9]
  1065. - fixed TSQLRestStorageInMemory.UpdateBlobFields() to return true
  1066. if no BLOB field is defined (as with TSQLRestServer) - ticket [bfa13889d5]
  1067. - fixed issue in TSQLRestStorageInMemory.GetJSONValues(), and handle
  1068. optional LIMIT clause in this very same method
  1069. - added new TSQLRestStorageInMemory.DropValues method
  1070. - fix potential GDI handle resource leak in TSQLRestClientURIMessage.Create
  1071. - introducing TSQLRestClientURIMessage.DoNotProcessMessages property
  1072. - TSQLRestClientURINamedPipe.InternalCheckOpen/InternalURI refactoring
  1073. - allow TSQLRestServer.ServiceRegister() to register an existing instance
  1074. of a class for a shared service - feature request [6e8b2ff3e9]
  1075. - allow TSQLRestServer.ExportServerMessage to be started in conjunction
  1076. with other protocols (like named pipes)
  1077. - added STATICFILE_CONTENT_TYPE[_HEADER] as aliases to HTTP_RESP_STATICFILE
  1078. as defined in SynCrtSock.pas unit, for generic handling
  1079. - added TSQLRestServer.Shutdown method for clean server stop - [55d5babb16]
  1080. - added TSQLRestServer.SessionsSaveToFile/SessionsLoadFromFile methods and
  1081. optional aStateFileName parameter to TSQLRestServer.Shutdown to allow
  1082. session persistence as requested by [a392945901] - warning: not for SOA!
  1083. - TSQLRestServerStats refactored and renamed TSQLRestServerMonitor so that
  1084. it follows the TSynMonitor way of doing statistics - also added several
  1085. properties as requested by feature request [4a2433c045]
  1086. - introducing detailed SOA statistics for method-based and interface-based
  1087. services, available from the TSQLRestServer.ServiceMethodStat[] property
  1088. or the associated TServiceFactoryServer.Stats / Stat[] methods, or
  1089. remotely as an option to the TSQLRestServer.Stat() service
  1090. - fixed potential errors JSON generation issue in TSQLRestServer.URI
  1091. (ticket [b0e9116aeb])
  1092. - TSQLRestServer.LaunchCallBack() is now inlined in TSQLRestServer.URI()
  1093. - fixed ticket [a5e3564e48] about RecordRef typecast (and enhance comments)
  1094. - fixed ticket [4f4dd18ad9] about TPropInfo.IsStored not handling methods
  1095. callbacks, e.g. for TPersistent storage
  1096. - fixed ticket [21c2d5ae96] when inserting/updating blob-only table content
  1097. - fixed ticket [7e9f06bf1a] to let TSQLTable.FieldLengthMax() use caption
  1098. text for enumeration columns
  1099. - fixed ticket [28545a4ce0] about TSQLRestStorageInMemory.EngineDelete
  1100. not thread-safe when run directly on server side
  1101. - fixed ticket [027bb9678d] - now TSQLRecordRTree class works as expected
  1102. - fixed ticket [876a097316] about TSQLRest.Add() when ForcedID<>0
  1103. - added DoNotAutoComputeFields optional param to TSQLRest(Batch).Add/Update
  1104. - implement ticket [e3f9742865] for enhanced JSON in woHumanReadable mode
  1105. - fixed GPF issue in TServiceFactoryServer after instance time-out deletion
  1106. - added TSQLPropInfo.PropertyIndex member
  1107. - added TSQLRecordProperties.SimpleFieldsCount[] array
  1108. - added TSQLRecordProperties.FieldBits[] field index map for all types
  1109. - added TSQLRecordProperties.SmallFieldsBits property
  1110. - added TSQLRecordProperties.FieldBitsFromCSV()/FieldBitsFromRawUTF8()
  1111. methods (with functions ready to be used e.g. in BatchAdd/BatchUpdate),
  1112. and TSQLRecordProperties.FieldBitsFromBlobField() method
  1113. - added TSQLRecordProperties.RegisterCustomFixedSizeRecordProperty() and
  1114. RegisterCustomRTTIRecordProperty() methods
  1115. - added TSQLRecordProperties.SetCustomCollationForAll() +
  1116. SetCustomCollation() methods, and TSQLModel.SetCustomCollationForAll()
  1117. to implement ticket [bfdc198e70]
  1118. - introducing TSQLRecordProperties.SetMaxLengthValidatorForTextFields() and
  1119. SetMaxLengthFilterForTextFields() methods, and also corresponding
  1120. TSQLModel.SetMaxLength[Validator/Filter]ForAllTextFields() methods
  1121. so that text column lengths may be checked or truncated before sending
  1122. to an external database expecting a maximum length
  1123. - fixed issue in TSQLRecordProperties.SetSimpleFieldsExpandedJSONWriter()
  1124. when the record contains some TCreateTime published field type
  1125. - added TSQLTable.GetAsInt64() method (proposal [3bea5d89c6])
  1126. - added TSQLTable.GetAsFloat() GetAsCurrency() GetAsDateTime() methods
  1127. - JSON parsing will now expect true, false or null to be in lowercase
  1128. (as in json.org specifications)
  1129. - SetWeakZero() function will now use a much faster per-class lock design
  1130. - exposed StatusCodeToErrorMsg() function
  1131. - extraction of TTestLowLevelTypes and TTestBasicClasses code into
  1132. SynSelfTests.pas unit
  1133. - allow only to delete its own session - security fix for ticket [7723fa7ebd]
  1134. - variant published properties will use getter/setter - ticket [479938b694]
  1135. - double/currency published properties will use getter/setter as expected
  1136. - fix TSQLRestClientURI.Commit/RollBack to work as expected
  1137. - added optional RaiseException parameter to TSQLRest.Commit for [fa702c126a]
  1138. - introducing TSQLRestServer.AuthenticationRegister/AuthenticationUnregister
  1139. methods and associated TSQLRestServerAuthentication* classes, used also by
  1140. TSQLRestClientURI.SetUser() to allow generic class-driven authentication
  1141. schemes for feature request [8c8a2a880c]
  1142. - added TSQLRestServerAuthentication.Options, e.g. saoUserByLogonOrID to
  1143. allow login via TSQLAuthUser.ID in addition to LogonName
  1144. - return also "logongroup":TSQLAuthGroup.ID on successful authentication
  1145. - added TSQLRestServerAuthenticationSignedURI.NoTimeStampCoherencyCheck and
  1146. TimeStampCoherencySeconds properties to tune or disable the session
  1147. timestamp check during URI signature authentication (default to 5 seconds)
  1148. - new TSQLRestServerAuthenticationNone weak but simple method
  1149. - force almost-random session ID for TSQLRestServer to avoid collision
  1150. after server restart
  1151. - stronger client-generated nonce for TSQLRestServerAuthenticationDefault
  1152. - ORM/SOA threads will display a friendly name in the IDE for [6acfd0a3d3]
  1153. - new TSynMonitor class, for easy statistics gathering of any process:
  1154. will be shared by framework's ORM, SOA and DDD implementation
  1155. - introducing TSQLRestServerKind enumeration to identify the kind of
  1156. TSQLRestServer instance running (SQlite3/static/virtual) for a table
  1157. - TSQLRestServer.SessionGetUser method is now made public (e.g. when
  1158. calling CurrentServiceContext.Factory.RestServer.SessionGetUser)
  1159. - added TSQLRestClientURI.OnIdle property, to enable more responsive
  1160. User Interface in case of slow network - feature request [68337ae98a]
  1161. - introducing InternalClassPropInfo() as wrapper around InternalClassProp()
  1162. - replaced confusing TVarData by a new dedicated TSQLVar memory structure,
  1163. shared with SynDB and mORMotSQLite3 units (includes methods refactoring)
  1164. *)
  1165. {$I Synopse.inc} // define HASINLINE USETYPEINFO CPU32 CPU64
  1166. {.$define PUREPASCAL} // define for debugg, not on production
  1167. {.$define USETYPEINFO} // define for debugg, not on production
  1168. {$ifdef MSWINDOWS}
  1169. {.$define ANONYMOUSNAMEDPIPE}
  1170. // if defined, the security attributes of the named pipe will use an
  1171. // anonymous connection - it should allow access to a service initialized
  1172. // named pipe on a remote computer.
  1173. // - I tried to implement the code as detailed in this Microsoft article:
  1174. // http://support.microsoft.com/kb/813414 but it didn't work as
  1175. // expected: see our forum http://synopse.info/forum/viewtopic.php?id=43
  1176. // - don't define it, because it's still buggy, and consider using HTTP
  1177. // connection for remote access over the network
  1178. {$define NOSECURITYFORNAMEDPIPECLIENTS}
  1179. // define this may avoid issues with Delphi XE+ for obscure reasons
  1180. {.$define SSPIAUTH}
  1181. // if defined, the Windows built-in authentication will be used
  1182. // along with the usual one
  1183. // - If you pass to TSQLRestClientURI.SetUser an empty string as user name,
  1184. // the Windows authentication will be performed
  1185. // - In this case, in table TSQLAuthUser should be an entry for the
  1186. // windows user, with the LoginName in form 'DomainName\UserName'
  1187. {$endif}
  1188. interface
  1189. uses
  1190. {$ifdef MSWINDOWS}
  1191. Windows,
  1192. Messages,
  1193. {$endif}
  1194. {$ifdef KYLIX3}
  1195. Types,
  1196. LibC,
  1197. SynKylix,
  1198. {$endif}
  1199. {$ifdef UNICODE}
  1200. Generics.Collections,
  1201. {$endif}
  1202. Classes,
  1203. SynZip, // use crc32 for TSQLRestClientURI.SetUser
  1204. {$ifdef USETYPEINFO}
  1205. // some pure pascal version must handle the 64-bits ordinal values or
  1206. // a not-Delphi RTTI layout of the underlying compiler (e.g. FPC)
  1207. TypInfo,
  1208. {$ifdef FPC}
  1209. SynFPCTypInfo, // small wrapper unit around FPC's TypInfo.pp
  1210. {$endif}
  1211. {$endif}
  1212. {$ifndef LVCL}
  1213. SyncObjs, // for TEvent and TCriticalSection
  1214. Contnrs, // for TObjectList
  1215. {$ifndef NOVARIANTS}
  1216. Variants,
  1217. {$endif}
  1218. {$endif LVCL}
  1219. SysUtils,
  1220. {$ifdef SSPIAUTH}
  1221. SynSSPIAuth,
  1222. {$endif}
  1223. SynCommons,
  1224. SynLog,
  1225. SynTests;
  1226. { ************ low level types and constants for handling JSON and fields }
  1227. { Why use JSON? (extracted from the main framework documentation)
  1228. - The JavaScript Object Notation (JSON) is a lightweight computer data
  1229. interchange format
  1230. - Like XML, it's a text-based, human-readable format for representing
  1231. simple data structures and associative arrays (called objects)
  1232. - It's easier to read, quicker to implement and smaller in size than XML
  1233. - It's a very efficient format for cache
  1234. - It's natively supported by the JavaScript language, making it a perfect
  1235. serialization format for any Ajax application
  1236. - The JSON format is specified in http://tools.ietf.org/html/rfc4627
  1237. - The default text encoding for both JSON and SQLite3 is UTF-8, which
  1238. allows the full Unicode charset to be stored and communicated
  1239. - It is the default data format used by ASP.NET AJAX services created in
  1240. Windows Communication Foundation (WCF) since .NET framework 3.5
  1241. - For binary blob transmission, we simply encode the binary data as hexa
  1242. using the SQLite3 BLOB literals format : hexadecimal data preceded by
  1243. a single "x" or "X" character (for example: X'53514C697465'), or Base64
  1244. encoding - see BlobToTSQLRawBlob() function }
  1245. const
  1246. /// maximum number of Tables in a Database Model
  1247. // - this constant is used internaly to optimize memory usage in the
  1248. // generated asm code
  1249. // - you should not change it to a value lower than expected in an existing
  1250. // database (e.g. as expected by TSQLAccessRights or such)
  1251. MAX_SQLTABLES = 256;
  1252. type
  1253. /// this is the type to be used for our ORM primary key, i.e. TSQLRecord.ID
  1254. // - it maps the SQLite3 RowID definition
  1255. // - when converted to plain TSQLRecord published properties, you may loose
  1256. // some information under Win32 when stored as a 32 bit pointer
  1257. // - could be defined as value in a TSQLRecord property as such:
  1258. // ! property AnotherRecord: TID read fAnotherRecord write fAnotherRecord;
  1259. TID = type Int64;
  1260. /// a pointer to a ORM primary key, i.e. TSQLRecord.ID: TID
  1261. PID = ^TID;
  1262. /// used to store a dynamic array of ORM primary keys, i.e. TSQLRecord.ID
  1263. TIDDynArray = array of TID;
  1264. /// used to store bit set for all available Tables in a Database Model
  1265. TSQLFieldTables = set of 0..MAX_SQLTABLES-1;
  1266. /// a String used to store the BLOB content
  1267. // - equals RawByteString for byte storage, to force no implicit charset
  1268. // conversion, whatever the codepage of the resulting string is
  1269. // - will identify a sftBlob field type, if used to define such a published
  1270. // property
  1271. // - by default, the BLOB fields are not retrieved or updated with raw
  1272. // TSQLRest.Retrieve() method, that is "Lazy loading" is enabled by default
  1273. // for blobs, unless TSQLRestClientURI.ForceBlobTransfert property is TRUE
  1274. // (for all tables), or ForceBlobTransfertTable[] (for a particular table);
  1275. // so use RetrieveBlob() methods for handling BLOB fields
  1276. // - could be defined as value in a TSQLRecord property as such:
  1277. // ! property Blob: TSQLRawBlob read fBlob write fBlob;
  1278. TSQLRawBlob = type RawByteString;
  1279. /// a reference to another record in any table in the database Model
  1280. // - stored as a 64-bit signed integer (just like the TID type)
  1281. // - type cast any value of TRecordReference with the RecordRef object below
  1282. // for easy access to its content
  1283. // - use TSQLRest.Retrieve(Reference) to get a record value
  1284. // - don't change associated TSQLModel tables order, since TRecordReference
  1285. // depends on it to store the Table type in its highest bits
  1286. // - when the pointed record will be deleted, this property will be set to 0
  1287. // by TSQLRestServer.AfterDeleteForceCoherency()
  1288. // - could be defined as value in a TSQLRecord property as such:
  1289. // ! property AnotherRecord: TRecordReference read fAnotherRecord write fAnotherRecord;
  1290. TRecordReference = type Int64;
  1291. /// a reference to another record in any table in the database Model
  1292. // - stored as a 64-bit signed integer (just like the TID type)
  1293. // - type cast any value of TRecordReference with the RecordRef object below
  1294. // for easy access to its content
  1295. // - use TSQLRest.Retrieve(Reference) to get a record value
  1296. // - don't change associated TSQLModel tables order, since TRecordReference
  1297. // depends on it to store the Table type in its highest bits
  1298. // - when the pointed record will be deleted, any record containg a matching
  1299. // property will be deleted by TSQLRestServer.AfterDeleteForceCoherency()
  1300. // - could be defined as value in a TSQLRecord property as such:
  1301. // ! property AnotherRecord: TRecordReferenceToBeDeleted
  1302. // ! read fAnotherRecord write fAnotherRecord;
  1303. TRecordReferenceToBeDeleted = type TRecordReference;
  1304. /// an Int64-encoded date and time of the latest update of a record
  1305. // - can be used as published property field in TSQLRecord for sftModTime:
  1306. // if any such property is defined in the table, it will be auto-filled with
  1307. // the server timestamp corresponding to the latest record update
  1308. // - use internally for computation an abstract "year" of 16 months of 32 days
  1309. // of 32 hours of 64 minutes of 64 seconds - faster than TDateTime
  1310. // - use TimeLogFromDateTime/TimeLogToDateTime/TimeLogNow/Iso8601ToTimeLog
  1311. // functions, or type-cast the value with a TTimeLogBits memory structure for
  1312. // direct access to its bit-oriented content (or via PTimeLogBits pointer)
  1313. // - could be defined as value in a TSQLRecord property as such:
  1314. // ! property LastModif: TModTime read fLastModif write fLastModif;
  1315. TModTime = type TTimeLog;
  1316. /// an Int64-encoded date and time of the record creation
  1317. // - can be used as published property field in TSQLRecord for sftCreateTime:
  1318. // if any such property is defined in the table, it will be auto-filled with
  1319. // the server timestamp corresponding to the record creation
  1320. // - use internally for computation an abstract "year" of 16 months of 32 days
  1321. // of 32 hours of 64 minutes of 64 seconds - faster than TDateTime
  1322. // - use TimeLogFromDateTime/TimeLogToDateTime/TimeLogNow/Iso8601ToTimeLog
  1323. // functions, or type-cast the value with a TTimeLogBits memory structure for
  1324. // direct access to its bit-oriented content (or via PTimeLogBits pointer)
  1325. // - could be defined as value in a TSQLRecord property as such:
  1326. // ! property CreatedAt: TModTime read fCreatedAt write fCreatedAt;
  1327. TCreateTime = type TTimeLog;
  1328. /// the Int64/TID of the TSQLAuthUser currently logged
  1329. // - can be used as published property field in TSQLRecord for sftSessionUserID:
  1330. // if any such property is defined in the table, it will be auto-filled with
  1331. // the current TSQLAuthUser.ID value at update, or 0 if no session is running
  1332. // - could be defined as value in a TSQLRecord property as such:
  1333. // ! property User: TSessionUserID read fUser write fUser;
  1334. TSessionUserID = type TID;
  1335. /// a monotonic version number, used to track changes on a table
  1336. // - add such a published field to any TSQLRecord will allow tracking of
  1337. // record modifications - note that only a single field of this type should
  1338. // be defined for a given record
  1339. // - note that this published field is NOT part of the record "simple fields":
  1340. // by default, the version won't be retrieved from the DB, nor will be sent
  1341. // from a client - the Engine*() CRUD method will take care of computing the
  1342. // monotonic version number, just before storage to the persistence engine
  1343. // - such a field will use a separated TSQLRecordTableDeletion table to
  1344. // track the deleted items
  1345. // - could be defined as value in a TSQLRecord property as such:
  1346. // ! property TrackedVersion: TRecordVersion read fVersion write fVersion;
  1347. TRecordVersion = type Int64;
  1348. /// the available types for any SQL field property, as managed with the
  1349. // database driver
  1350. // - sftUnknown: unknown or not defined field type
  1351. // - sftAnsiText: a WinAnsi encoded TEXT, forcing a NOCASE collation
  1352. // (TSQLRecord Delphi property was declared as AnsiString or string before
  1353. // Delphi 2009)
  1354. // - sftUTF8Text is UTF-8 encoded TEXT, forcing a SYSTEMNOCASE collation,
  1355. // i.e. using UTF8IComp() (TSQLRecord property was declared as RawUTF8,
  1356. // RawUnicode or WideString - or string in Delphi 2009+)
  1357. //- sftEnumerate is an INTEGER value corresponding to an index in any
  1358. // enumerate Delphi type; storage is an INTEGER value (fast, easy and size
  1359. // efficient); at display, this integer index will be converted into the
  1360. // left-trimed lowercased chars of the enumerated type text conversion:
  1361. // TOpenType(1) = otDone -> 'Done'
  1362. /// - sftSet is an INTEGER value corresponding to a bitmapped set of
  1363. // enumeration; storage is an INTEGER value (fast, easy and size efficient);
  1364. // displayed as an integer by default, sets with an enumeration type with
  1365. // up to 64 elements is allowed yet (stored as an Int64)
  1366. // - sftInteger is an INTEGER (Int64 precision, as expected by SQLite3) field
  1367. // - sftID is an INTEGER field pointing to the ID/ROWID of another record of
  1368. // a table, defined by the class type of the TSQLRecord inherited property;
  1369. // coherency is always ensured: after a delete, all values pointing to
  1370. // it is reset to 0
  1371. // - sftRecord is an INTEGER field pointing to the ID/ROWID of another
  1372. // record: TRecordReference=Int64 Delphi property which can be typecasted to
  1373. // RecordRef; coherency is always ensured: after a delete, all values
  1374. // pointing to it are reset to 0 by the ORM
  1375. // - sftBoolean is an INTEGER field for a boolean value: 0 is FALSE,
  1376. // anything else TRUE (encoded as JSON 'true' or 'false' constants)
  1377. // - sftFloat is a FLOAT (floating point double precision, cf. SQLite3)
  1378. // field, defined as double (or single) published properties definition
  1379. // - sftDateTime is a ISO 8601 encoded (SQLite3 compatible) TEXT field,
  1380. // corresponding to a TDateTime Delphi property: a ISO8601 collation is
  1381. // forced for such column, for proper date/time sorting and searching
  1382. // - sftTimeLog is an INTEGER field for coding a date and time (not SQLite3
  1383. // compatible), which should be defined as TTimeLog=Int64 Delphi property,
  1384. // ready to be typecasted to the TTimeLogBits optimized type for efficient
  1385. // timestamp storage, with a second resolution
  1386. // - sftCurrency is a FLOAT containing a 4 decimals floating point value,
  1387. // compatible with the Currency Delphi type, which minimizes rounding errors
  1388. // in monetary calculations which may occur with sftFloat type
  1389. // - sftObject is a TEXT containing an ObjectToJSON serialization, able to
  1390. // handle published properties of any not TPersistent as JSON object,
  1391. // TStrings or TRawUTF8List as JSON arrays of strings, TCollection or
  1392. // TObjectList as JSON arrays of JSON objects
  1393. // - sftVariant is a TEXT containing a variant value encoded as JSON:
  1394. // string values are stored between quotes, numerical values directly stored,
  1395. // and JSON objects or arrays will be handled as TDocVariant custom types
  1396. // - sftNullable is a INTEGER/DOUBLE/TEXT field containing a NULLable value,
  1397. // stored as a local variant property, identifying TNullableInteger,
  1398. // TNullableBoolean, TNullableFloat, TNullableCurrency,
  1399. // TNullableDateTime, TNullableTimeLog and TNullableUTF8Text types
  1400. // - sftBlob is a BLOB field (TSQLRawBlob Delphi property), and won't be
  1401. // retrieved by default (not part of ORM "simple types"), to save bandwidth
  1402. // - sftBlobDynArray is a dynamic array, stored as BLOB field: this kind of
  1403. // property will be retrieved by default, i.e. is recognized as a "simple
  1404. // field", and will use Base64 encoding during JSON transmission, or a true
  1405. // JSON array, depending on the database back-end (e.g. MongoDB)
  1406. // - sftBlobCustom is a custom property, stored as BLOB field: such
  1407. // properties are defined by adding a TSQLPropInfoCustom instance, overriding
  1408. // TSQLRecord.InternalRegisterCustomProperties virtual method - they will
  1409. // be retrieved by default, i.e. recognized as "simple fields"
  1410. // - sftUTF8Custom is a custom property, stored as JSON in a TEXT field,
  1411. // defined by overriding TSQLRecord.InternalRegisterCustomProperties
  1412. // virtual method, and adding a TSQLPropInfoCustom instance, e.g. via
  1413. // RegisterCustomPropertyFromTypeName() or RegisterCustomPropertyFromRTTI();
  1414. // they will be retrieved by default, i.e. recognized as "simple fields"
  1415. // - sftMany is a 'many to many' field (TSQLRecordMany Delphi property);
  1416. // nothing is stored in the table row, but in a separate pivot table: so
  1417. // there is nothing to retrieve here; in contrast to other TSQLRecord
  1418. // published properties, which contains an INTEGER ID, the TSQLRecord.Create
  1419. // will instanciate a true TSQLRecordMany instance to handle this pivot table
  1420. // via its dedicated ManyAdd/FillMany/ManySelect methods - as a result, such
  1421. // properties won't be retrieved by default, i.e. not recognized as "simple
  1422. // fields" unless you used the dedicated methods
  1423. // - sftModTime is an INTEGER field containing the TModTime value, aka time
  1424. // of the record latest update; TModTime (just like TTimeLog or TCreateTime)
  1425. // published property can be typecasted to the TTimeLogBits memory structure;
  1426. // the value of this field is automatically updated with the current
  1427. // date and time each time a record is updated (with external DB, it will
  1428. // use the Server time, as retrieved from SynDB) - see ComputeFieldsBeforeWrite
  1429. // virtual method of TSQLRecord; note also that only RESTful PUT/POST access
  1430. // will change this field value: manual SQL statements (like
  1431. // 'UPDATE Table SET Column=0') won't change its content; note also that
  1432. // this is automated on Delphi client side, so only within TSQLRecord ORM use
  1433. // (a pure AJAX application should fill such fields explicitely before sending)
  1434. // - sftCreateTime is an INTEGER field containing the TCreateTime time
  1435. // of the record creation; TCreateTime (just like TTimeLog or TModTime)
  1436. // published property can be typecasted to the TTimeLogBits memory structure;
  1437. // the value of this field is automatically updated with the current
  1438. // date and time when the record is created (with external DB, it will
  1439. // use the Server time, as retrieved from SynDB) - see ComputeFieldsBeforeWrite
  1440. // virtual method of TSQLRecord; note also that only RESTful PUT/POST access
  1441. // will set this field value: manual SQL statements (like
  1442. // 'INSERT INTO Table ...') won't set its content; note also that this is
  1443. // automated on Delphi client side, so only within TSQLRecord ORM use (a
  1444. // pure AJAX application should fill such fields explicitely before sending)
  1445. // - sftTID is an INTEGER field containing a TID pointing to another record;
  1446. // since regular TSQLRecord published properties (i.e. sftID kind of field)
  1447. // can not be greater than 2,147,483,647 (i.e. a signed 32 bit value) under
  1448. // Win32, defining TID published properties would allow to store the ID
  1449. // as signed 64-bit, e.g. up to 9,223,372,036,854,775,808; despite to
  1450. // sftID kind of record, coherency is NOT ensured: after a deletion, all
  1451. // values pointing to are NOT reset to 0 - it is up to your business logic
  1452. // to ensure data coherency as expected
  1453. // - sftRecordVersion is an INTEGER field containing a TRecordVersion
  1454. // monotonic number: adding such a published field to any TSQLRecord will
  1455. // allow tracking of record modifications, at storage level; by design,
  1456. // such a field won't be part of "simple types", so won't be transmitted
  1457. // between the clients and the server, but will be updated at any write
  1458. // operation by the low-level Engine*() storage methods - such a field
  1459. // will use a TSQLRecordTableDeletion table to track the deleted items
  1460. // - sftSessionUserID is an INTEGER field containing the TSQLAuthUser.ID
  1461. // of the record modification; the value of this field is automatically
  1462. // updated with the current User ID of the active session; note also that
  1463. // only RESTful PUT/POST access will change this field value: manual SQL
  1464. // statements (like 'UPDATE Table SET Column=0') won't change its content;
  1465. // this is automated on Delphi client side, so only within TSQLRecord ORM use
  1466. // (a pure AJAX application should fill such fields explicitely before sending)
  1467. TSQLFieldType = (
  1468. sftUnknown,
  1469. sftAnsiText,
  1470. sftUTF8Text,
  1471. sftEnumerate,
  1472. sftSet,
  1473. sftInteger,
  1474. sftID,
  1475. sftRecord,
  1476. sftBoolean,
  1477. sftFloat,
  1478. sftDateTime,
  1479. sftTimeLog,
  1480. sftCurrency,
  1481. sftObject,
  1482. {$ifndef NOVARIANTS}
  1483. sftVariant,
  1484. sftNullable,
  1485. {$endif}
  1486. sftBlob,
  1487. sftBlobDynArray,
  1488. sftBlobCustom,
  1489. sftUTF8Custom,
  1490. sftMany,
  1491. sftModTime,
  1492. sftCreateTime,
  1493. sftTID,
  1494. sftRecordVersion,
  1495. sftSessionUserID);
  1496. /// set of available SQL field property types
  1497. TSQLFieldTypes = set of TSQLFieldType;
  1498. //// a fixed array of SQL field property types
  1499. TSQLFieldTypeArray = array[0..MAX_SQLFIELDS] of TSQLFieldType;
  1500. /// contains the parameters used for sorting
  1501. // - FieldCount is 0 if was never sorted
  1502. // - used to sort data again after a successfull data update with TSQLTableJSON.FillFrom()
  1503. TSQLTableSortParams = record
  1504. FieldCount, FieldIndex: integer;
  1505. FieldType: TSQLFieldType;
  1506. Asc: boolean;
  1507. end;
  1508. /// used to define the triggered Event types for TNotifySQLEvent
  1509. // - some Events can be triggered via TSQLRestServer.OnUpdateEvent when
  1510. // a Table is modified, and actions can be authorized via overriding the
  1511. // TSQLRest.RecordCanBeUpdated method
  1512. // - OnUpdateEvent is called BEFORE deletion, and AFTER insertion or update; it
  1513. // should be used only server-side, not to synchronize some clients: the framework
  1514. // is designed around a stateless RESTful architecture (like HTTP/1.1), in which
  1515. // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer)
  1516. // - is used also by TSQLRecord.ComputeFieldsBeforeWrite virtual method
  1517. TSQLEvent = (
  1518. seAdd,
  1519. seUpdate,
  1520. seDelete,
  1521. seUpdateBlob);
  1522. /// used to define the triggered Event types for TSQLRecordHistory
  1523. // - TSQLRecordHistory.History will be used for heArchiveBlob
  1524. // - TSQLRecordHistory.SentDataJSON will be used for other kind of events
  1525. TSQLHistoryEvent = (
  1526. heAdd,
  1527. heUpdate,
  1528. heDelete,
  1529. heArchiveBlob);
  1530. /// used to defined the CRUD associated SQL statement of a command
  1531. // - used e.g. by TSQLRecord.GetJSONValues methods and SimpleFieldsBits[] array
  1532. // (in this case, soDelete is never used, since deletion is global for all fields)
  1533. // - also used for cache content notification
  1534. TSQLOccasion = (
  1535. soSelect,
  1536. soInsert,
  1537. soUpdate,
  1538. soDelete);
  1539. /// used to defined a set of CRUD associated SQL statement of a command
  1540. TSQLOccasions = set of TSQLOccasion;
  1541. const
  1542. /// kind of fields not retrieved during normal query, update or adding
  1543. // - by definition, BLOB are excluded to save transmission bandwidth
  1544. // - by design, TSQLRecordMany properties are stored in an external pivot table
  1545. // - by convenience, the TRecordVersion number is for internal use only
  1546. NOT_SIMPLE_FIELDS: TSQLFieldTypes =
  1547. [sftUnknown,sftBlob,sftMany,sftRecordVersion];
  1548. /// kind of fields which can be copied from one TSQLRecord instance to another
  1549. COPIABLE_FIELDS: TSQLFieldTypes =
  1550. [low(TSQLFieldType)..high(TSQLFieldType)] - [sftUnknown, sftMany];
  1551. /// kind of DB fields which will contain TEXT content when converted to JSON
  1552. TEXT_DBFIELDS: TSQLDBFieldTypes = [ftUTF8,ftDate];
  1553. /// kind of fields which will contain pure TEXT values
  1554. // - independently from the actual storage level
  1555. // - i.e. will match RawUTF8, string, UnicodeString, WideString properties
  1556. RAWTEXT_FIELDS: TSQLFieldTypes = [sftAnsiText,sftUTF8Text];
  1557. {$ifndef NOVARIANTS}
  1558. type
  1559. /// define a variant published property as a nullable integer
  1560. // - either a varNull or a varInt64 value will be stored in the variant
  1561. // - either a NULL or an INTEGER value will be stored in the database
  1562. // - the property should be defined as such:
  1563. // ! property Int: TNullableInteger read fInt write fInt;
  1564. TNullableInteger = type variant;
  1565. /// define a variant published property as a nullable boolean
  1566. // - either a varNull or a varBoolean value will be stored in the variant
  1567. // - either a NULL or a 0/1 INTEGER value will be stored in the database
  1568. // - the property should be defined as such:
  1569. // ! property Bool: TNullableBoolean read fBool write fBool;
  1570. TNullableBoolean = type variant;
  1571. /// define a variant published property as a nullable floating point value
  1572. // - either a varNull or a varDouble value will be stored in the variant
  1573. // - either a NULL or a FLOAT value will be stored in the database
  1574. // - the property should be defined as such:
  1575. // ! property Flt: TNullableFloat read fFlt write fFlt;
  1576. TNullableFloat = type variant;
  1577. /// define a variant published property as a nullable decimal value
  1578. // - either a varNull or a varCurrency value will be stored in the variant
  1579. // - either a NULL or a FLOAT value will be stored in the database
  1580. // - the property should be defined as such:
  1581. // ! property Cur: TNullableCurrency read fCur write fCur;
  1582. TNullableCurrency = type variant;
  1583. /// define a variant published property as a nullable date/time value
  1584. // - either a varNull or a varDate value will be stored in the variant
  1585. // - either a NULL or a ISO-8601 TEXT value will be stored in the database
  1586. // - the property should be defined as such:
  1587. // ! property Dat: TNullableDateTime read fDat write fDat;
  1588. TNullableDateTime = type variant;
  1589. /// define a variant published property as a nullable timestamp value
  1590. // - either a varNull or a varInt64 value will be stored in the variant
  1591. // - either a NULL or a TTimeLog INTEGER value will be stored in the database
  1592. // - the property should be defined as such:
  1593. // ! property Tim: TNullableTimrency read fTim write fTim;
  1594. TNullableTimeLog = type variant;
  1595. /// define a variant published property as a nullable UTF-8 encoded text
  1596. // - either a varNull or varString (RawUTF8) will be stored in the variant
  1597. // - either a NULL or a TEXT value will be stored in the database
  1598. // - the property should be defined as such:
  1599. // ! property Txt: TNullableUTF8Text read fTxt write fTxt;
  1600. // or for a fixed-width VARCHAR (in external databases), here of 32 max chars:
  1601. // ! property Txt: TNullableUTF8Text index 32 read fTxt write fTxt;
  1602. // - warning: prior to Delphi 2009, since the variant will be stored as
  1603. // RawUTF8 internally, you should not use directly the field value as a
  1604. // VCL string=AnsiString like string(aField) but use VariantToString(aField)
  1605. TNullableUTF8Text = type variant;
  1606. const
  1607. /// the SQL field property types with their TNullable* equivalency
  1608. // - those types may be stored in a variant published property, e.g.
  1609. // ! property Int: TNullableInteger read fInt write fInt;
  1610. // ! property Txt: TNullableUTF8Text read fTxt write fTxt;
  1611. // ! property Txt: TNullableUTF8Text index 32 read fTxt write fTxt;
  1612. NULLABLE_TYPES = [sftInteger,sftBoolean,sftEnumerate,sftFloat,sftCurrency,
  1613. sftDateTime,sftTimeLog,sftUTF8Text];
  1614. /// creates a nullable integer value from a supplied constant
  1615. // - FPC does not allow direct assignment to a TNullableInteger = type variant
  1616. // variable: use this function to circumvent it
  1617. function NullableInteger(const Value: Int64): TNullableInteger;
  1618. {$ifdef HASINLINE}inline;{$endif}
  1619. var
  1620. /// a nullable integer value containing null
  1621. NullableIntegerNull: TNullableInteger absolute NullVarData;
  1622. /// a nullable boolean value containing null
  1623. NullableBooleanNull: TNullableBoolean absolute NullVarData;
  1624. /// a nullable float value containing null
  1625. NullableFloatNull: TNullableFloat absolute NullVarData;
  1626. /// a nullable currency value containing null
  1627. NullableCurrencyNull: TNullableCurrency absolute NullVarData;
  1628. /// a nullable TDateTime value containing null
  1629. NullableDateTimeNull: TNullableDateTime absolute NullVarData;
  1630. /// a nullable TTimeLog value containing null
  1631. NullableTimeLogNull: TNullableTimeLog absolute NullVarData;
  1632. /// a nullable UTF-8 encoded text value containing null
  1633. NullableUTF8TextNull: TNullableUTF8Text absolute NullVarData;
  1634. /// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
  1635. // - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
  1636. // direct transtyping from a TNullableInteger = type variant variant: use this
  1637. // function to circumvent those limitations
  1638. function NullableIntegerIsEmptyOrNull(const V: TNullableInteger): Boolean;
  1639. {$ifdef HASINLINE}inline;{$endif}
  1640. /// check if a TNullableInteger is null, or return its value
  1641. // - returns FALSE if V is null or empty, or TRUE and set the Integer value
  1642. function NullableIntegerToValue(const V: TNullableInteger; out Value: Int64): Boolean;
  1643. overload; {$ifdef HASINLINE}inline;{$endif}
  1644. /// check if a TNullableInteger is null, or return its value
  1645. // - returns 0 if V is null or empty, or the stored Integer value
  1646. function NullableIntegerToValue(const V: TNullableInteger): Int64;
  1647. overload; {$ifdef HASINLINE}inline;{$endif}
  1648. /// creates a nullable Boolean value from a supplied constant
  1649. // - FPC does not allow direct assignment to a TNullableBoolean = type variant
  1650. // variable: use this function to circumvent it
  1651. function NullableBoolean(Value: boolean): TNullableBoolean;
  1652. {$ifdef HASINLINE}inline;{$endif}
  1653. /// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
  1654. // - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
  1655. // direct transtyping from a TNullableBoolean = type variant variant: use this
  1656. // function to circumvent those limitations
  1657. function NullableBooleanIsEmptyOrNull(const V: TNullableBoolean): Boolean;
  1658. {$ifdef HASINLINE}inline;{$endif}
  1659. /// check if a TNullableBoolean is null, or return its value
  1660. // - returns FALSE if V is null or empty, or TRUE and set the Boolean value
  1661. function NullableBooleanToValue(const V: TNullableBoolean; out Value: Boolean): Boolean;
  1662. overload; {$ifdef HASINLINE}inline;{$endif}
  1663. /// check if a TNullableBoolean is null, or return its value
  1664. // - returns false if V is null or empty, or the stored Boolean value
  1665. function NullableBooleanToValue(const V: TNullableBoolean): Boolean;
  1666. overload; {$ifdef HASINLINE}inline;{$endif}
  1667. /// creates a nullable floating-point value from a supplied constant
  1668. // - FPC does not allow direct assignment to a TNullableFloat = type variant
  1669. // variable: use this function to circumvent it
  1670. function NullableFloat(const Value: double): TNullableFloat;
  1671. {$ifdef HASINLINE}inline;{$endif}
  1672. /// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
  1673. // - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
  1674. // direct transtyping from a TNullableFloat = type variant variant: use this
  1675. // function to circumvent those limitations
  1676. function NullableFloatIsEmptyOrNull(const V: TNullableFloat): Boolean;
  1677. {$ifdef HASINLINE}inline;{$endif}
  1678. /// check if a TNullableFloat is null, or return its value
  1679. // - returns FALSE if V is null or empty, or TRUE and set the Float value
  1680. function NullableFloatToValue(const V: TNullableFloat; out Value: double): boolean;
  1681. overload; {$ifdef HASINLINE}inline;{$endif}
  1682. /// check if a TNullableFloat is null, or return its value
  1683. // - returns 0 if V is null or empty, or the stored Float value
  1684. function NullableFloatToValue(const V: TNullableFloat): double;
  1685. overload; {$ifdef HASINLINE}inline;{$endif}
  1686. /// creates a nullable Currency value from a supplied constant
  1687. // - FPC does not allow direct assignment to a TNullableCurrency = type variant
  1688. // variable: use this function to circumvent it
  1689. function NullableCurrency(const Value: currency): TNullableCurrency;
  1690. {$ifdef HASINLINE}inline;{$endif}
  1691. /// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
  1692. // - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
  1693. // direct transtyping from a TNullableCurrency = type variant variant: use this
  1694. // function to circumvent those limitations
  1695. function NullableCurrencyIsEmptyOrNull(const V: TNullableCurrency): Boolean;
  1696. {$ifdef HASINLINE}inline;{$endif}
  1697. /// check if a TNullableCurrency is null, or return its value
  1698. // - returns FALSE if V is null or empty, or TRUE and set the Currency value
  1699. function NullableCurrencyToValue(const V: TNullableCurrency; out Value: currency): boolean;
  1700. overload; {$ifdef HASINLINE}inline;{$endif}
  1701. /// check if a TNullableCurrency is null, or return its value
  1702. // - returns 0 if V is null or empty, or the stored Currency value
  1703. function NullableCurrencyToValue(const V: TNullableCurrency): currency;
  1704. overload; {$ifdef HASINLINE}inline;{$endif}
  1705. /// creates a nullable TDateTime value from a supplied constant
  1706. // - FPC does not allow direct assignment to a TNullableDateTime = type variant
  1707. // variable: use this function to circumvent it
  1708. function NullableDateTime(const Value: TDateTime): TNullableDateTime;
  1709. {$ifdef HASINLINE}inline;{$endif}
  1710. /// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
  1711. // - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
  1712. // direct transtyping from a TNullableDateTime = type variant variant: use this
  1713. // function to circumvent those limitations
  1714. function NullableDateTimeIsEmptyOrNull(const V: TNullableDateTime): Boolean;
  1715. {$ifdef HASINLINE}inline;{$endif}
  1716. /// check if a TNullableDateTime is null, or return its value
  1717. // - returns FALSE if V is null or empty, or TRUE and set the DateTime value
  1718. function NullableDateTimeToValue(const V: TNullableDateTime; out Value: TDateTime): boolean;
  1719. overload; {$ifdef HASINLINE}inline;{$endif}
  1720. /// check if a TNullableDateTime is null, or return its value
  1721. // - returns 0 if V is null or empty, or the stored DateTime value
  1722. function NullableDateTimeToValue(const V: TNullableDateTime): TDateTime;
  1723. overload; {$ifdef HASINLINE}inline;{$endif}
  1724. /// creates a nullable TTimeLog value from a supplied constant
  1725. // - FPC does not allow direct assignment to a TNullableTimeLog = type variant
  1726. // variable: use this function to circumvent it
  1727. function NullableTimeLog(const Value: TTimeLog): TNullableTimeLog;
  1728. {$ifdef HASINLINE}inline;{$endif}
  1729. /// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
  1730. // - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
  1731. // direct transtyping from a TNullableTimeLog = type variant variant: use this
  1732. // function to circumvent those limitations
  1733. function NullableTimeLogIsEmptyOrNull(const V: TNullableTimeLog): Boolean;
  1734. {$ifdef HASINLINE}inline;{$endif}
  1735. /// check if a TNullableTimeLog is null, or return its value
  1736. // - returns FALSE if V is null or empty, or TRUE and set the TimeLog value
  1737. function NullableTimeLogToValue(const V: TNullableTimeLog; out Value: TTimeLog): boolean;
  1738. overload; {$ifdef HASINLINE}inline;{$endif}
  1739. /// check if a TNullableTimeLog is null, or return its value
  1740. // - returns 0 if V is null or empty, or the stored TimeLog value
  1741. function NullableTimeLogToValue(const V: TNullableTimeLog): TTimeLog;
  1742. overload; {$ifdef HASINLINE}inline;{$endif}
  1743. /// creates a nullable UTF-8 encoded text value from a supplied constant
  1744. // - FPC does not allow direct assignment to a TNullableUTF8 = type variant
  1745. // variable: use this function to circumvent it
  1746. function NullableUTF8Text(const Value: RawUTF8): TNullableUTF8Text;
  1747. {$ifdef HASINLINE}inline;{$endif}
  1748. /// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
  1749. // - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
  1750. // direct transtyping from a TNullableUTF8Text = type variant variant: use this
  1751. // function to circumvent those limitations
  1752. function NullableUTF8TextIsEmptyOrNull(const V: TNullableUTF8Text): Boolean;
  1753. {$ifdef HASINLINE}inline;{$endif}
  1754. /// check if a TNullableUTF8Text is null, or return its value
  1755. // - returns FALSE if V is null or empty, or TRUE and set the UTF8Text value
  1756. function NullableUTF8TextToValue(const V: TNullableUTF8Text; out Value: RawUTF8): boolean;
  1757. overload; {$ifdef HASINLINE}inline;{$endif}
  1758. /// check if a TNullableUTF8Text is null, or return its value
  1759. // - returns '' if V is null or empty, or the stored UTF8-encoded text value
  1760. function NullableUTF8TextToValue(const V: TNullableUTF8Text): RawUTF8;
  1761. overload; {$ifdef HASINLINE}inline;{$endif}
  1762. {$endif NOVARIANTS}
  1763. /// similar to AddInt64() function, but for a TIDDynArray
  1764. // - some random GPF were identified with AddInt64(TInt64DynArray(Values),...)
  1765. // with the Delphi Win64 compiler
  1766. procedure AddID(var Values: TIDDynArray; var ValuesCount: integer; Value: TID);
  1767. type
  1768. /// the available options for TSQLRest.BatchStart() process
  1769. // - boInsertOrIgnore will create 'INSERT OR IGNORE' statements instead of
  1770. // plain 'INSERT' - by now, only the direct mORMotSQLite3 engine supports it
  1771. // - boInsertOrUpdate will create 'INSERT OR REPLACE' statements instead of
  1772. // plain 'INSERT' - by now, only the direct mORMotSQLite3 engine supports it
  1773. // - boExtendedJSON would force the JSON to unquote the column names,
  1774. // e.g. writing col1:...,col2:... instead of "col1":...,"col2"...
  1775. // - boPostNoSimpleFields would avoid to send a TSQLRestBach.Add() with simple
  1776. // fields as "SIMPLE":[val1,val2...] or "SIMPLE@tablename":[val1,val2...],
  1777. // without the field names
  1778. TSQLRestBatchOption = (
  1779. boInsertOrIgnore, boInsertOrReplace,
  1780. boExtendedJSON, boPostNoSimpleFields);
  1781. /// a set of options for TSQLRest.BatchStart() process
  1782. // - TJSONObjectDecoder will use it to compute the corresponding SQL
  1783. TSQLRestBatchOptions = set of TSQLRestBatchOption;
  1784. /// define how TJSONObjectDecoder.Decode() will handle JSON string values
  1785. TJSONObjectDecoderParams = (pInlined, pQuoted, pNonQuoted);
  1786. /// define how TJSONObjectDecoder.FieldTypeApproximation[] is identified
  1787. TJSONObjectDecoderFieldType = (
  1788. ftaNumber,ftaBoolean,ftaString,ftaDate,ftaNull,ftaBlob,ftaObject,ftaArray);
  1789. /// JSON object decoding and SQL generation, in the context of ORM process
  1790. // - this is the main process for marshalling JSON into SQL statements
  1791. // - used e.g. by GetJSONObjectAsSQL() function or ExecuteFromJSON and
  1792. // InternalBatchStop methods
  1793. TJSONObjectDecoder = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  1794. /// contains the decoded field names
  1795. FieldNames: array[0..MAX_SQLFIELDS-1] of RawUTF8;
  1796. /// contains the decoded field values
  1797. FieldValues: array[0..MAX_SQLFIELDS-1] of RawUTF8;
  1798. /// Decode() will set each field type approximation
  1799. // - will recognize also JSON_BASE64_MAGIC/JSON_SQLDATE_MAGIC prefix
  1800. FieldTypeApproximation: array[0..MAX_SQLFIELDS-1] of TJSONObjectDecoderFieldType;
  1801. /// number of fields decoded in FieldNames[] and FieldValues[]
  1802. FieldCount: integer;
  1803. /// set to TRUE if parameters are to be :(...): inlined
  1804. InlinedParams: boolean;
  1805. /// internal pointer over field names to be used after Decode() call
  1806. // - either FieldNames, either Fields[] array as defined in Decode()
  1807. DecodedFieldNames: PRawUTF8Array;
  1808. /// the ID=.. value as sent within the JSON object supplied to Decode()
  1809. DecodedRowID: TID;
  1810. /// decode the JSON object fields into FieldNames[] and FieldValues[]
  1811. // - if Fields=nil, P should be a true JSON object, i.e. defined
  1812. // as "COL1"="VAL1" pairs, stopping at '}' or ']'; otherwise, Fields[]
  1813. // contains column names and expects a JSON array as "VAL1","VAL2".. in P
  1814. // - P should be after the initial '{' or '[' character, i.e. at first field
  1815. // - P returns the next object start or nil on unexpected end of input
  1816. // - P^ buffer will let the JSON be decoded in-place, so consider using
  1817. // the overloaded Decode(JSON: RawUTF8; ...) method
  1818. // - FieldValues[] strings will be quoted and/or inlined depending on Params
  1819. // - if RowID is set, a RowID column will be added within the returned content
  1820. procedure Decode(var P: PUTF8Char; const Fields: TRawUTF8DynArray;
  1821. Params: TJSONObjectDecoderParams; const RowID: TID=0; ReplaceRowIDWithID: Boolean=false); overload;
  1822. /// decode the JSON object fields into FieldNames[] and FieldValues[]
  1823. // - overloaded method expecting a RawUTF8 buffer, making a private copy
  1824. // of the JSON content to avoid unexpected in-place modification, then
  1825. // calling Decode(P: PUTF8Char) to perform the process
  1826. procedure Decode(const JSON: RawUTF8; const Fields: TRawUTF8DynArray;
  1827. Params: TJSONObjectDecoderParams; const RowID: TID=0; ReplaceRowIDWithID: Boolean=false); overload;
  1828. /// can be used after Decode() to add a new field in FieldNames/FieldValues
  1829. // - so that EncodeAsSQL() will include this field in the generated SQL
  1830. // - caller should ensure that the FieldName is not already defined in
  1831. // FieldNames[] (e.g. when the TRecordVersion field is forced)
  1832. // - the caller should ensure that the supplied FieldValue will match
  1833. // the quoting/inlining expectations of Decode(TJSONObjectDecoderParams) -
  1834. // e.g. that string values are quoted if needed
  1835. procedure AddFieldValue(const FieldName,FieldValue: RawUTF8;
  1836. FieldType: TJSONObjectDecoderFieldType);
  1837. /// encode as a SQL-ready INSERT or UPDATE statement
  1838. // - after a successfull call to Decode()
  1839. // - escape SQL strings, according to the official SQLite3 documentation
  1840. // (i.e. ' inside a string is stored as '')
  1841. // - if InlinedParams was TRUE, it will create prepared parameters like
  1842. // 'COL1=:("VAL1"):, COL2=:(VAL2):'
  1843. // - called by GetJSONObjectAsSQL() function or TSQLRestStorageExternal
  1844. function EncodeAsSQL(Update: boolean): RawUTF8;
  1845. /// encode as a SQL-ready INSERT or UPDATE statement with ? as values
  1846. // - after a successfull call to Decode()
  1847. // - FieldValues[] content will be ignored
  1848. // - Occasion can be only soInsert or soUpdate
  1849. // - for soUpdate, will create UPDATE ... SET ... where UpdateIDFieldName=?
  1850. // - you can specify some options, e.g. boInsertOrIgnore for soInsert
  1851. function EncodeAsSQLPrepared(const TableName: RawUTF8; Occasion: TSQLOccasion;
  1852. const UpdateIDFieldName: RawUTF8; BatchOptions: TSQLRestBatchOptions): RawUTF8;
  1853. /// encode the FieldNames/FieldValues[] as a JSON object
  1854. procedure EncodeAsJSON(out result: RawUTF8);
  1855. /// set the specified array to the fields names
  1856. // - after a successfull call to Decode()
  1857. procedure AssignFieldNamesTo(var Fields: TRawUTF8DynArray);
  1858. /// returns TRUE if the specified array match the decoded fields names
  1859. // - after a successfull call to Decode()
  1860. function SameFieldNames(const Fields: TRawUTF8DynArray): boolean;
  1861. /// search for a field name in the current identified FieldNames[]
  1862. function FindFieldName(const FieldName: RawUTF8): integer;
  1863. end;
  1864. /// set the TID (=64 bits integer) value from the numerical text stored in P^
  1865. // - just a redirection to SynCommons.SetInt64()
  1866. procedure SetID(P: PUTF8Char; var result: TID); overload;
  1867. {$ifdef HASINLINE}inline;{$endif}
  1868. /// set the TID (=64 bits integer) value from the numerical text stored in U
  1869. // - just a redirection to SynCommons.SetInt64()
  1870. procedure SetID(const U: RawByteString; var result: TID); overload;
  1871. {$ifdef HASINLINE}inline;{$endif}
  1872. /// decode JSON fields object into an UTF-8 encoded SQL-ready statement
  1873. // - this function decodes in the P^ buffer memory itself (no memory allocation
  1874. // or copy), for faster process - so take care that it is an unique string
  1875. // - P should be after the initial '{' or '[' character, i.e. at first field
  1876. // - P contains the next object start or nil on unexpected end of input
  1877. // - if Fields is void, expects expanded "COL1"="VAL1" pairs in P^, stopping at '}' or ']'
  1878. // - otherwise, Fields[] contains the column names and expects "VAL1","VAL2".. in P^
  1879. // - returns 'COL1="VAL1", COL2=VAL2' if UPDATE is true (UPDATE SET format)
  1880. // - returns '(COL1, COL2) VALUES ("VAL1", VAL2)' otherwise (INSERT format)
  1881. // - escape SQL strings, according to the official SQLite3 documentation
  1882. // (i.e. ' inside a string is stored as '')
  1883. // - if InlinedParams is set, will create prepared parameters like
  1884. // 'COL1=:("VAL1"):, COL2=:(VAL2):'
  1885. // - if RowID is set, a RowID column will be added within the returned content
  1886. function GetJSONObjectAsSQL(var P: PUTF8Char; const Fields: TRawUTF8DynArray;
  1887. Update, InlinedParams: boolean; RowID: TID=0; ReplaceRowIDWithID: Boolean=false): RawUTF8; overload;
  1888. /// decode JSON fields object into an UTF-8 encoded SQL-ready statement
  1889. // - is used e.g. by TSQLRestServerDB.EngineAdd/EngineUpdate methods
  1890. // - expect a regular JSON expanded object as "COL1"="VAL1",...} pairs
  1891. // - make its own temporary copy of JSON data before calling GetJSONObjectAsSQL() above
  1892. // - returns 'COL1="VAL1", COL2=VAL2' if UPDATE is true (UPDATE SET format)
  1893. // - returns '(COL1, COL2) VALUES ("VAL1", VAL2)' otherwise (INSERT format)
  1894. // - if InlinedParams is set, will create prepared parameters like 'COL2=:(VAL2):'
  1895. // - if RowID is set, a RowID column will be added within the returned content
  1896. function GetJSONObjectAsSQL(const JSON: RawUTF8; Update, InlinedParams: boolean;
  1897. RowID: TID=0; ReplaceRowIDWithID: Boolean=false): RawUTF8; overload;
  1898. /// get the FIRST field value of the FIRST row, from a JSON content
  1899. // - e.g. useful to get an ID without converting a JSON content into a TSQLTableJSON
  1900. function UnJSONFirstField(var P: PUTF8Char): RawUTF8;
  1901. /// returns TRUE if the JSON content is in expanded format
  1902. // - i.e. as plain [{"ID":10,"FirstName":"John","LastName":"Smith"}...]
  1903. // - i.e. not as '{"fieldCount":3,"values":["ID","FirstName","LastName",...']}
  1904. function IsNotAjaxJSON(P: PUTF8Char): Boolean;
  1905. /// retrieve a JSON '{"Name":Value,....}' object
  1906. // - P is nil in return in case of an invalid object
  1907. // - returns the UTF-8 encoded JSON object, including first '{' and last '}'
  1908. // - if ExtractID is set, it will contain the "ID":203 field value, and this
  1909. // field won't be included in the resulting UTF-8 encoded JSON object unless
  1910. // KeepIDField is true
  1911. // - this function expects this "ID" property to be the FIRST in the
  1912. // "Name":Value pairs, as generated by TSQLRecord.GetJSONValues(W)
  1913. function JSONGetObject(var P: PUTF8Char; ExtractID: PID;
  1914. var EndOfObject: AnsiChar; KeepIDField: boolean): RawUTF8;
  1915. /// retrieve the ID/RowID field of a JSON object
  1916. // - this function expects this "ID" property to be the FIRST in the
  1917. // "Name":Value pairs, as generated by TSQLRecord.GetJSONValues(W)
  1918. // - returns TRUE if ID/RowID has been found, and set ID with the value
  1919. function JSONGetID(P: PUTF8Char; out ID: TID): Boolean;
  1920. /// fill a TSQLRawBlob from TEXT-encoded blob data
  1921. // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or
  1922. // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT
  1923. function BlobToTSQLRawBlob(P: PUTF8Char): TSQLRawBlob; overload;
  1924. /// fill a TSQLRawBlob from TEXT-encoded blob data
  1925. // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or
  1926. // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT
  1927. function BlobToTSQLRawBlob(const Blob: RawByteString): TSQLRawBlob; overload;
  1928. /// create a TBytes from TEXT-encoded blob data
  1929. // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or
  1930. // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT
  1931. function BlobToBytes(P: PUTF8Char): TBytes;
  1932. /// create a memory stream from TEXT-encoded blob data
  1933. // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or
  1934. // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT
  1935. // - the caller must free the stream instance after use
  1936. function BlobToStream(P: PUTF8Char): TStream;
  1937. /// creates a TEXT-encoded version of blob data from a TSQLRawBlob
  1938. // - TEXT will be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.)
  1939. function TSQLRawBlobToBlob(const RawBlob: TSQLRawBlob): RawUTF8; overload;
  1940. {$ifdef HASINLINE}inline;{$endif}
  1941. /// creates a TEXT-encoded version of blob data from a memory data
  1942. // - same as TSQLRawBlob, but with direct memory access via a pointer/byte size pair
  1943. // - TEXT will be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.)
  1944. function TSQLRawBlobToBlob(RawBlob: pointer; RawBlobLength: integer): RawUTF8; overload;
  1945. /// convert a Base64-encoded content into binary hexadecimal ready for SQL
  1946. // - returns e.g. X'53514C697465'
  1947. procedure Base64MagicToBlob(Base64: PUTF8Char; var result: RawUTF8);
  1948. /// return true if the TEXT is encoded as SQLite3 BLOB literals (X'53514C697465' e.g.)
  1949. function isBlobHex(P: PUTF8Char): boolean;
  1950. {$ifdef HASINLINE}inline;{$endif}
  1951. /// compute the SQL corresponding to a WHERE clause
  1952. // - returns directly the Where value if it starts with one the
  1953. // ORDER/GROUP/LIMIT/OFFSET/JOIN keywords
  1954. // - otherwise, append ' WHERE '+Where
  1955. function SQLFromWhere(const Where: RawUTF8): RawUTF8;
  1956. /// find out if the supplied WHERE clause starts with one of the
  1957. // ORDER/GROUP/LIMIT/OFFSET/JOIN keywords
  1958. function SQLWhereIsEndClause(const Where: RawUTF8): boolean;
  1959. /// naive search of '... FROM TableName ...' pattern in the supplied SQL
  1960. function GetTableNameFromSQLSelect(const SQL: RawUTF8;
  1961. EnsureUniqueTableInFrom: boolean): RawUTF8;
  1962. /// naive search of '... FROM Table1,Table2 ...' pattern in the supplied SQL
  1963. function GetTableNamesFromSQLSelect(const SQL: RawUTF8): TRawUTF8DynArray;
  1964. /// guess the content type of an UTF-8 encoded field value, as used in TSQLTable.Get()
  1965. // - if P if nil or 'null', return sftUnknown
  1966. // - otherwise, guess its type from its value characters
  1967. // - sftBlob is returned if the field is encoded as SQLite3 BLOB literals
  1968. // (X'53514C697465' e.g.) or with '\uFFF0' magic code
  1969. // - since P is PUTF8Char, string type is sftUTF8Text only
  1970. // - sftFloat is returned for any floating point value, even if it was
  1971. // declared as sftCurrency type
  1972. // - sftInteger is returned for any INTEGER stored value, even if it was declared
  1973. // as sftEnumerate, sftSet, sftID, sftTID, sftRecord, sftRecordVersion,
  1974. // sftSessionUserID, sftBoolean or sftModTime / sftCreateTime / sftTimeLog type
  1975. function UTF8ContentType(P: PUTF8Char): TSQLFieldType;
  1976. /// guess the number type of an UTF-8 encoded field value, as used in TSQLTable.Get()
  1977. // - if P if nil or 'null', return sftUnknown
  1978. // - will return sftInteger or sftFloat if the supplied text is a number
  1979. // - will return sftUTF8Text for any non numerical content
  1980. function UTF8ContentNumberType(P: PUTF8Char): TSQLFieldType;
  1981. {$ifdef HASINLINE}inline;{$endif}
  1982. /// read an object properties, as saved by TINIWriter.WriteObject() method
  1983. // - i.e. only Integer, Int64, enumerates (including boolean), floating point,
  1984. // variant and (Ansi/Wide/Unicode)String properties (excluding shortstring)
  1985. // - read only the published properties of the current class level (do NOT
  1986. // read the properties content published in the parent classes)
  1987. // - "From" must point to the [section] containing the object properties
  1988. // - for integers and enumerates, if no value is stored in From (or From is ''),
  1989. // the default value from the property definition is set
  1990. procedure ReadObject(Value: TObject; From: PUTF8Char; const SubCompName: RawUTF8=''); overload;
  1991. /// read an object properties, as saved by TINIWriter.WriteObject() method
  1992. // - i.e. only Integer, Int64, enumerates (including boolean), floating point values
  1993. // and (Ansi/Wide/Unicode)String properties (excluding shortstring)
  1994. // - read only the published properties of the current class level (do NOT
  1995. // read the properties content published in the parent classes)
  1996. // - for integers, if no value is stored in FromContent, the default value is set
  1997. // - this version gets the appropriate section from [Value.ClassName]
  1998. // - this version doesn't handle embedded objects
  1999. procedure ReadObject(Value: TObject; const FromContent: RawUTF8;
  2000. const SubCompName: RawUTF8=''); overload;
  2001. /// write an object properties, as saved by TINIWriter.WriteObject() method
  2002. // - i.e. only Integer, Int64, enumerates (including boolean), floating point values
  2003. // and (Ansi/Wide/Unicode)String properties (excluding shortstring)
  2004. // - write only the published properties of the current class level (do NOT
  2005. // write the properties content published in the parent classes)
  2006. // - direct update of INI-like content
  2007. // - for integers, value is always written, even if matches the default value
  2008. procedure WriteObject(Value: TObject; var IniContent: RawUTF8;
  2009. const Section: RawUTF8; const SubCompName: RawUTF8=''); overload;
  2010. /// write an object properties, as saved by TINIWriter.WriteObject() method
  2011. // - i.e. only Integer, Int64, enumerates (including boolean), floating point values
  2012. // and (Ansi/Wide/Unicode)String properties (excluding shortstring)
  2013. // - write only the published properties of the current class level (do NOT
  2014. // write the properties content published in the parent classes)
  2015. // - return the properties as text Name=Values pairs, with no section
  2016. // - for integers, if the value matches the default value, it is not added to the result
  2017. function WriteObject(Value: TObject): RawUTF8; overload;
  2018. /// copy object properties
  2019. // - copy Integer, Int64, enumerates (including boolean), variant, records,
  2020. // dynamic arrays, classes and any string properties (excluding shortstring)
  2021. // - TCollection items can be copied also, if they are of the same exact class
  2022. // - object properties instances are created in aTo if the objects are not
  2023. // TSQLRecord children (in this case, these are not class instances, but
  2024. // INTEGER reference to records, so only the integer value is copied), that is
  2025. // for regular Delphi classes
  2026. procedure CopyObject(aFrom, aTo: TObject); overload;
  2027. /// create a new object instance, from an existing one
  2028. // - will create a new instance of the same class, then call the overloaded
  2029. // CopyObject() procedure to copy its values
  2030. function CopyObject(aFrom: TObject): TObject; overload;
  2031. /// copy two TStrings instances
  2032. // - will just call Dest.Assign(Source) in practice
  2033. procedure CopyStrings(Source, Dest: TStrings);
  2034. {$ifndef LVCL}
  2035. /// copy two TCollection instances
  2036. // - will call CopyObject() in loop to repopulate the Dest collection,
  2037. // which would work even if Assign() method was not overriden
  2038. procedure CopyCollection(Source, Dest: TCollection);
  2039. {$endif}
  2040. /// set any default integer or enumerates (including boolean) published
  2041. // properties values for a TPersistent/TSynPersistent
  2042. // - set only the values set as "property ... default ..." at class type level
  2043. // - will also reset the published properties of the nested classes
  2044. procedure SetDefaultValuesObject(Value: TObject);
  2045. /// will reset all the object properties to their default
  2046. // - strings would be set to '', numbers to 0
  2047. // - if FreeAndNilNestedObjects is the default FALSE, will recursively reset
  2048. // all nested class properties values
  2049. // - if FreeAndNilNestedObjects is TRUE, will FreeAndNil() all the nested
  2050. // class properties
  2051. // - for a TSQLRecord, use its ClearProperties method instead, which will
  2052. // handle the ID property, and any nested JOINed instances
  2053. procedure ClearObject(Value: TObject; FreeAndNilNestedObjects: boolean=false);
  2054. /// persist a class instance into a JSON file
  2055. // - returns TRUE on success, false on error (e.g. the file name is invalid
  2056. // or the file is existing and could not be overwritten)
  2057. function ObjectToJSONFile(Value: TObject; const JSONFile: TFileName;
  2058. Options: TTextWriterWriteObjectOptions=[woHumanReadable]): boolean;
  2059. /// will serialize any TObject into its expanded UTF-8 JSON representation
  2060. // - includes debugger-friendly information, similar to TSynLog, i.e.
  2061. // class name and sets/enumerates as text
  2062. // - could be used to create a TDocVariant object with full information
  2063. // - wrapper around ObjectToJSON(Value,[woDontStoreDefault,woFullExpand])
  2064. // also able to serialize plain Exception as a simple '{"Exception":"Message"}'
  2065. function ObjectToJSONDebug(Value: TObject): RawUTF8;
  2066. /// will serialize any TObject into a TDocVariant debugging document
  2067. // - just a wrapper around _JsonFast(ObjectToJSONDebug()) with an optional
  2068. // "Context":"..." text message
  2069. // - if the supplied context format matches '{....}' then it will be added
  2070. // as a corresponding TDocVariant JSON object
  2071. function ObjectToVariantDebug(Value: TObject;
  2072. const ContextFormat: RawUTF8; const ContextArgs: array of const;
  2073. const ContextName: RawUTF8='context'): variant; overload;
  2074. /// will serialize any TObject into a TDocVariant debugging document
  2075. // - just a wrapper around _JsonFast(ObjectToJSONDebug())
  2076. function ObjectToVariantDebug(Value: TObject): variant; overload;
  2077. /// add the property values of a TObject to a document-based object content
  2078. // - if Obj is a TDocVariant object, then all Values's published
  2079. // properties will be added at the root level of Obj
  2080. procedure _ObjAddProps(Value: TObject; var Obj: variant); overload;
  2081. /// is able to compare two objects by value
  2082. // - both instances may or may not be of the same class, but properties
  2083. // should match
  2084. // - will use direct RTTI access of property values, or TSQLRecord.SameValues()
  2085. // if available to make the comparison as fast and accurate as possible
  2086. // - if you want only to compare the plain fields with no getter function,
  2087. // e.g. if they are just some conversion of the same information, you can
  2088. // set ignoreGetterFields=TRUE
  2089. function ObjectEquals(Value1,Value2: TObject; ignoreGetterFields: boolean=false): boolean;
  2090. type
  2091. /// available options for JSONToObject() parsing process
  2092. // - by default, function will fail if a JSON field name is not part of the
  2093. // object published properties, unless j2oIgnoreUnknownProperty is defined
  2094. // - by default, function will check that the supplied JSON value would
  2095. // be a JSON string when the property is a string, unless j2oIgnoreStringType
  2096. // is defined and JSON numbers are accepted and stored as text
  2097. // - by default any unexpected value for enumerations would be marked as
  2098. // invalid, unless j2oIgnoreUnknownEnum is defined, so that in such case the
  2099. // ordinal 0 value is left, and loading continues
  2100. // - by default, only simple kind of variant types (string/numbers) are
  2101. // handled: set j2oHandleCustomVariants if you want to handle any custom -
  2102. // in this case , it will handle direct JSON [array] of {object}: but if you
  2103. // also define j2oHandleCustomVariantsWithinString, it will also try to
  2104. // un-escape a JSON string first, i.e. handle "[array]" or "{object}" content
  2105. // (may be used e.g. when JSON has been retrieved from a database TEXT column)
  2106. // - by default, a temporary instance would be created if a published field
  2107. // has a setter, and the instance is expected to be released later by the
  2108. // owner class: set j2oSetterExpectsToFreeTempInstance to let JSONToObject
  2109. // (and TPropInfo.ClassFromJSON) release it when the setter returns
  2110. TJSONToObjectOption = (
  2111. j2oIgnoreUnknownProperty, j2oIgnoreStringType, j2oIgnoreUnknownEnum,
  2112. j2oHandleCustomVariants, j2oHandleCustomVariantsWithinString,
  2113. j2oSetterExpectsToFreeTempInstance);
  2114. /// set of options for JSONToObject() parsing process
  2115. TJSONToObjectOptions = set of TJSONToObjectOption;
  2116. const
  2117. /// some open-minded options for JSONToObject() parsing
  2118. // - won't block JSON unserialization due to some minor class type definitions
  2119. // - used e.g. by TObjArraySerializer.CustomReader and
  2120. // TServiceMethodExecute.ExecuteJson methods
  2121. JSONTOOBJECT_TOLERANTOPTIONS = [j2oHandleCustomVariants,
  2122. j2oIgnoreUnknownEnum,j2oIgnoreUnknownProperty,j2oIgnoreStringType];
  2123. /// read an object properties, as saved by ObjectToJSON function
  2124. // - ObjectInstance must be an existing TObject instance
  2125. // - the data inside From^ is modified in-place (unescaped and transformed):
  2126. // calling JSONToObject(pointer(JSONRawUTF8)) would change the JSONRawUTF8
  2127. // variable content, which may not be what you expect - consider using the
  2128. // ObjectLoadJSON() function instead
  2129. // - handle Integer, Int64, enumerate (including boolean), set, floating point,
  2130. // TDateTime, TCollection, TStrings, TRawUTF8List, variant, and string properties
  2131. // (excluding ShortString, but including WideString and UnicodeString under
  2132. // Delphi 2009+)
  2133. // - TList won't be handled since it may leak memory when calling TList.Clear
  2134. // - won't handle TObjectList (even if ObjectToJSON is able to serialize
  2135. // them) since has no way of knowing the object type to add (TCollection.Add
  2136. // is missing), unless: 1. you set the TObjectListItemClass property as expected,
  2137. // and provide a TObjectList object, or 2. woStoreClassName option has been
  2138. // used at ObjectToJSON() call and the corresponding classes have been previously
  2139. // registered by TJSONSerializer.RegisterClassForJSON() (or Classes.RegisterClass)
  2140. // - will clear any previous TCollection objects, and convert any null JSON
  2141. // basic type into nil - e.g. if From='null', will call FreeAndNil(Value)
  2142. // - you can add some custom (un)serializers for ANY Delphi class, via the
  2143. // TJSONSerializer.RegisterCustomSerializer() class method
  2144. // - set Valid=TRUE on success, Valid=FALSE on error, and the main function
  2145. // will point in From at the syntax error place (e.g. on any unknown property name)
  2146. // - caller should explicitely perform a SetDefaultValuesObject(Value) if
  2147. // the default values are expected to be set before JSON parsing
  2148. function JSONToObject(var ObjectInstance; From: PUTF8Char; var Valid: boolean;
  2149. TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): PUTF8Char;
  2150. /// read an object properties, as saved by ObjectToJSON function
  2151. // - ObjectInstance must be an existing TObject instance
  2152. // - this overloaded version will make a private copy of the supplied JSON
  2153. // content (via TSynTempBuffer), to ensure the original buffer won't be modified
  2154. // during process, before calling safely JSONToObject()
  2155. // - will return TRUE on success, or FALSE if the supplied JSON was invalid
  2156. function ObjectLoadJSON(var ObjectInstance; const JSON: RawUTF8;
  2157. TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean;
  2158. /// create a new object instance, as saved by ObjectToJSON(...,[...,woStoreClassName,...]);
  2159. // - JSON input should be either 'null', either '{"ClassName":"TMyClass",...}'
  2160. // - woStoreClassName option shall have been used at ObjectToJSON() call
  2161. // - and the corresponding class shall have been previously registered by
  2162. // TJSONSerializer.RegisterClassForJSON(), in order to retrieve the class type
  2163. // from it name - or, at least, by a Classes.RegisterClass() function call
  2164. // - the data inside From^ is modified in-place (unescaped and transformed):
  2165. // don't call JSONToObject(pointer(JSONRawUTF8)) but makes a temporary copy of
  2166. // the JSON text buffer before calling this function, if want to reuse it later
  2167. function JSONToNewObject(var From: PUTF8Char; var Valid: boolean;
  2168. Options: TJSONToObjectOptions=[]): TObject;
  2169. /// decode a specified parameter compatible with URI encoding into its original
  2170. // object contents
  2171. // - ObjectInstance must be an existing TObject instance
  2172. // - will call internaly JSONToObject() function to unserialize its content
  2173. // - UrlDecodeExtended('price=20.45&where=LastName%3D%27M%C3%B4net%27','PRICE=',P,@Next)
  2174. // will return Next^='where=...' and P=20.45
  2175. // - if Upper is not found, Value is not modified, and result is FALSE
  2176. // - if Upper is found, Value is modified with the supplied content, and result is TRUE
  2177. function UrlDecodeObject(U: PUTF8Char; Upper: PAnsiChar; var ObjectInstance; Next: PPUTF8Char=nil;
  2178. Options: TJSONToObjectOptions=[]): boolean;
  2179. /// fill the object properties from a JSON file content
  2180. // - ObjectInstance must be an existing TObject instance
  2181. // - this function will call RemoveCommentsFromJSON() before process
  2182. function JSONFileToObject(const JSONFile: TFileName; var ObjectInstance;
  2183. TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean;
  2184. { ************ some RTTI and SQL mapping routines }
  2185. type
  2186. /// the class kind as handled by TClassInstance object
  2187. TClassInstanceItemCreate = (
  2188. cicUnknown,cicTSQLRecord,cicTObjectList,cicTPersistentWithCustomCreate,
  2189. cicTSynPersistent,cicTInterfacedCollection,cicTInterfacedObjectWithCustomCreate,
  2190. cicTCollection,cicTCollectionItem,cicTComponent,cicTObject);
  2191. /// store information about a class, able to easily create new instances
  2192. // - using this temporary storage would speed up the creation process
  2193. // - any virtual constructor would be used, including for TCollection types
  2194. TClassInstance = object
  2195. public
  2196. /// the class type itself
  2197. ItemClass: TClass;
  2198. // how the class instance is expected to be created
  2199. ItemCreate: TClassInstanceItemCreate;
  2200. {$ifndef LVCL}
  2201. /// for TCollection instances, the associated TCollectionItem class
  2202. CollectionItemClass: TCollectionItemClass;
  2203. {$endif}
  2204. /// fill the internal information fields for a given class type
  2205. procedure Init(C: TClass);
  2206. /// create a new instance of the registered class
  2207. function CreateNew: TObject;
  2208. end;
  2209. /// points to information about a class, able to create new instances
  2210. PClassInstance = ^TClassInstance;
  2211. { type definitions below were adapted from TypInfo.pas
  2212. - this implementation doesn't require to include Variant.pas any more (which
  2213. allow easy server-side compile with LVCL, e.g.)
  2214. - some code was rewritten in an object orientation manner (declared as objects
  2215. instead of records) to avoid use of global function/procedure
  2216. - allows easy published properties enumeration with ClassProp()
  2217. - if a property doesn't have a write attribute (i.e. no setter), its value
  2218. is set using the field adress itself (from read f* getter)
  2219. - some useful but not implemented functions were added in optimized assembler }
  2220. type
  2221. {$ifdef FPC}
  2222. /// available type families for Free Pascal RTTI values
  2223. // - values differs from Delphi, and are taken from FPC typinfo.pp unit
  2224. TTypeKind = (tkUnknown,tkInteger,tkChar,tkEnumeration,tkFloat,
  2225. tkSet,tkMethod,tkSString,tkLString,tkAString,
  2226. tkWString,tkVariant,tkArray,tkRecord,tkInterface,
  2227. tkClass,tkObject,tkWChar,tkBool,tkInt64,tkQWord,
  2228. tkDynArray,tkInterfaceRaw,tkProcVar,tkUString,tkUChar,tkHelper);
  2229. const
  2230. // maps record or object types
  2231. tkRecordTypes = [tkObject,tkRecord];
  2232. {$else}
  2233. /// available type families for Delphi 6 and up
  2234. TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
  2235. tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
  2236. tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray
  2237. {$ifdef UNICODE}, tkUString{$endif});
  2238. const
  2239. // maps record or object types
  2240. tkRecordTypes = [tkRecord];
  2241. {$endif}
  2242. // maps long string types
  2243. tkStringTypes =
  2244. [tkLString,tkWString{$ifdef HASVARUSTRING},tkUString{$endif}{$ifdef FPC},tkAString{$endif}];
  2245. // maps 1, 8, 16, 32 and 64-bit ordinal types
  2246. tkOrdinalTypes =
  2247. [tkInteger, tkChar, tkWChar, tkEnumeration, tkSet, tkInt64
  2248. {$ifdef FPC},tkBool,tkQWord{$endif}];
  2249. type
  2250. /// specify ordinal (tkInteger and tkEnumeration) storage size and sign
  2251. // - note: Int64 is stored as its own TTypeKind, not as tkInteger
  2252. TOrdType = (otSByte, otUByte, otSWord, otUWord, otSLong, otULong);
  2253. /// specify floating point (ftFloat) storage size and precision
  2254. // - here ftDouble is renamed ftDoub to avoid confusion with TSQLDBFieldType
  2255. TFloatType = (ftSingle, ftDoub, ftExtended, ftComp, ftCurr);
  2256. TTypeKinds = set of TTypeKind;
  2257. PTypeKind = ^TTypeKind;
  2258. PTypeInfo = ^TTypeInfo;
  2259. {$ifdef HASDIRECTTYPEINFO}
  2260. PPTypeInfo = PTypeInfo;
  2261. {$else}
  2262. PPTypeInfo = ^PTypeInfo;
  2263. {$endif}
  2264. PTypeInfoDynArray = array of PTypeInfo;
  2265. TClassDynArray = array of TClass;
  2266. {$ifdef FPC}
  2267. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  2268. {$PACKRECORDS C}
  2269. {$else}
  2270. {$A-}
  2271. {$endif}
  2272. {$else}
  2273. {$A-} { Delphi compiler use packed storage for this internal types, not aligned data }
  2274. {$endif}
  2275. PPropInfo = ^TPropInfo;
  2276. PMethodInfo = ^TMethodInfo;
  2277. /// used to store a chain of properties RTTI
  2278. // - could be used e.g. by TSQLPropInfo to handled flattened properties
  2279. PPropInfoDynArray = array of PPropInfo;
  2280. /// pointer to TClassProp
  2281. PClassProp = ^TClassProp;
  2282. /// a wrapper to published properties of a class
  2283. // - start enumeration by getting a PClassProp with ClassProp()
  2284. // - use PropCount, P := @PropList to get the first PPropInfo, and then P^.Next
  2285. // - this enumeration is very fast and doesn't require any temporary memory,
  2286. // as in the TypInfo.GetPropInfos() PPropList usage
  2287. // - for TSQLRecord, you should better use the RecordProps.Fields[] array,
  2288. // which is faster and contains the properties published in parent classes
  2289. {$ifndef ISDELPHI2010}
  2290. TClassProp = object
  2291. {$else}
  2292. TClassProp = record
  2293. {$endif}
  2294. /// number of published properties in this object
  2295. PropCount: Word;
  2296. /// point to a TPropInfo packed array
  2297. // - layout is as such, with variable TPropInfo storage size:
  2298. // ! PropList: array[1..PropCount] of TPropInfo
  2299. // - use TPropInfo.Next to get the next one:
  2300. // ! P := @PropList;
  2301. // ! for i := 1 to PropCount do begin
  2302. // ! // ... do something with P
  2303. // ! P := P^.Next;
  2304. // ! end;
  2305. PropList: record end;
  2306. /// retrieve a Field property RTTI information from a Property Name
  2307. function FieldProp(const PropName: shortstring): PPropInfo;
  2308. end;
  2309. PClassType = ^TClassType;
  2310. /// a wrapper to class type information, as defined by the Delphi RTTI
  2311. {$ifndef ISDELPHI2010}
  2312. TClassType = object
  2313. {$else}
  2314. TClassType = record
  2315. {$endif}
  2316. /// the class type
  2317. ClassType: TClass;
  2318. /// the parent class type information
  2319. ParentInfo: PPTypeInfo;
  2320. /// the number of published properties
  2321. PropCount: SmallInt;
  2322. /// the name (without .pas extension) of the unit were the class was defined
  2323. // - then the PClassProp follows: use the method ClassProp to retrieve its
  2324. // address
  2325. UnitName: string[255];
  2326. /// get the information about the published properties of this class
  2327. // - stored after UnitName memory
  2328. function ClassProp: PClassProp;
  2329. {$ifdef HASINLINE}inline;{$endif}
  2330. /// fast and easy find if this class inherits from a specific class type
  2331. // - you should rather consider using TTypeInfo.InheritsFrom directly
  2332. function InheritsFrom(AClass: TClass): boolean;
  2333. /// return the size (in bytes) of this class type information
  2334. // - can be used to create class types at runtime
  2335. function RTTISize: integer;
  2336. end;
  2337. PEnumType = ^TEnumType;
  2338. /// a wrapper to enumeration type information, as defined by the Delphi RTTI
  2339. // - we use this to store the enumeration values as integer, but easily provide
  2340. // a text equivalent, translated if necessary, from the enumeration type
  2341. // definition itself
  2342. {$ifndef ISDELPHI2010}
  2343. TEnumType = object
  2344. {$else}
  2345. TEnumType = record
  2346. {$endif}
  2347. /// specify ordinal storage size and sign
  2348. // - is prefered to MaxValue to identify the number of stored bytes
  2349. OrdType: TOrdType;
  2350. { this seemingly extraneous inner record is here for alignment purposes, so
  2351. that its data gets aligned properly (if FPC_REQUIRES_PROPER_ALIGNMENT is set) }
  2352. {$ifdef FPC_ENUMHASINNER}
  2353. inner:
  2354. {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}
  2355. packed
  2356. {$endif FPC_ENUMHASINNER}
  2357. record
  2358. {$endif}
  2359. {$ifdef FPC_ENUMHASINNER}
  2360. iMinValue: Longint;
  2361. iMaxValue: Longint;
  2362. iBaseType: PPTypeInfo;
  2363. end;
  2364. {$else}
  2365. /// first value of enumeration type, typicaly 0
  2366. MinValue: Longint;
  2367. /// same as ord(high(type)): not the enumeration count, but the highest index
  2368. MaxValue: Longint;
  2369. /// the base type of this enumeration
  2370. /// - always use PEnumType(typeinfo(TEnumType))^.BaseType or more useful
  2371. // method PTypeInfo(typeinfo(TEnumType))^.EnumBaseType before calling
  2372. // any of the methods below
  2373. BaseType: PPTypeInfo;
  2374. {$endif FPC_ENUMHASINNER}
  2375. /// a concatenation of shortstrings, containing the enumeration names
  2376. NameList: string[255];
  2377. {$ifdef FPC_ENUMHASINNER}
  2378. function MinValue: Longint; inline;
  2379. function MaxValue: Longint; inline;
  2380. function BaseType: PPTypeInfo; inline;
  2381. {$endif FPC_ENUMHASINNER}
  2382. /// get the corresponding enumeration name
  2383. // - return the first one if Value is invalid (>MaxValue)
  2384. function GetEnumNameOrd(Value: Integer): PShortString;
  2385. /// get the corresponding enumeration name
  2386. // - return the first one if Value is invalid (>MaxValue)
  2387. // - Value will be converted to the matching ordinal value (byte or word)
  2388. function GetEnumName(const Value): PShortString;
  2389. {$ifdef HASINLINE}inline;{$endif}
  2390. /// retrieve all element names as a dynamic array of RawUTF8
  2391. // - names could be optionally trimmed left from their initial lower chars
  2392. procedure GetEnumNameAll(var result: TRawUTF8DynArray; TrimLeftLowerCase: boolean); overload;
  2393. /// retrieve all element names as CSV, with optional quotes
  2394. procedure GetEnumNameAll(var result: RawUTF8; const Prefix: RawUTF8='';
  2395. quotedValues: boolean=false); overload;
  2396. /// get all enumeration names as a JSON array of strings
  2397. function GetEnumNameAllAsJSONArray(TrimLeftLowerCase: boolean): RawUTF8;
  2398. /// get the corresponding enumeration ordinal value, from its name
  2399. // - if EnumName does start with lowercases 'a'..'z', they will be searched:
  2400. // e.g. GetEnumNameValue('sllWarning') will find sllWarning item
  2401. // - if Value does not start with lowercases 'a'..'z', they will be ignored:
  2402. // e.g. GetEnumNameValue('Warning') will find sllWarning item
  2403. // - return -1 if not found (don't use directly this value to avoid any GPF)
  2404. function GetEnumNameValue(const EnumName: ShortString): Integer; overload;
  2405. {$ifdef HASINLINE}inline;{$endif}
  2406. /// get the corresponding enumeration ordinal value, from its name
  2407. // - if Value does start with lowercases 'a'..'z', they will be searched:
  2408. // e.g. GetEnumNameValue('sllWarning') will find sllWarning item
  2409. // - if Value does not start with lowercases 'a'..'z', they will be ignored:
  2410. // e.g. GetEnumNameValue('Warning') will find sllWarning item
  2411. // - return -1 if not found (don't use directly this value to avoid any GPF)
  2412. function GetEnumNameValue(Value: PUTF8Char): Integer; overload;
  2413. {$ifdef HASINLINE}inline;{$endif}
  2414. /// get the corresponding enumeration ordinal value, from its name
  2415. // - if Value does start with lowercases 'a'..'z', they will be searched:
  2416. // e.g. GetEnumNameValue('sllWarning') will find sllWarning item
  2417. // - if AlsoTrimLowerCase is TRUE, and EnumName does not start with
  2418. // lowercases 'a'..'z', they will be ignored: e.g. GetEnumNameValue('Warning')
  2419. // will find sllWarning item
  2420. // - return -1 if not found (don't use directly this value to avoid any GPF)
  2421. function GetEnumNameValue(Value: PUTF8Char; ValueLen: integer;
  2422. AlsoTrimLowerCase: boolean=true): Integer; overload;
  2423. /// get the corresponding enumeration name, without the first lowercase chars
  2424. // (otDone -> 'Done')
  2425. // - Value will be converted to the matching ordinal value (byte or word)
  2426. function GetEnumNameTrimed(const Value): RawUTF8;
  2427. {$ifdef HASINLINE}inline;{$endif}
  2428. /// get the enumeration names corresponding to a set value
  2429. function GetSetNameCSV(Value: integer; SepChar: AnsiChar=',';
  2430. FullSetsAsStar: boolean=false): RawUTF8; overload;
  2431. /// get the enumeration names corresponding to a set value
  2432. procedure GetSetNameCSV(W: TTextWriter; Value: integer; SepChar: AnsiChar=',';
  2433. FullSetsAsStar: boolean=false); overload;
  2434. /// get the enumeration names corresponding to a set value, as a JSON array
  2435. function GetSetNameAsDocVariant(Value: integer; FullSetsAsStar: boolean=false): variant;
  2436. /// get the corresponding caption name, without the first lowercase chars
  2437. // (otDone -> 'Done')
  2438. // - return "string" type, i.e. UnicodeString for Delphi 2009+
  2439. // - internally call UnCamelCase() then System.LoadResStringTranslate() if available
  2440. // - Value will be converted to the matching ordinal value (byte or word)
  2441. function GetCaption(const Value): string;
  2442. /// get all caption names, ready to be display, as lines separated by #13#10
  2443. // - return "string" type, i.e. UnicodeString for Delphi 2009+
  2444. // - if UsedValuesBits is not nil, only the corresponding bits set are added
  2445. function GetCaptionStrings(UsedValuesBits: Pointer=nil): string;
  2446. /// add caption names, ready to be display, to a TStrings class
  2447. // - add pointer(ord(element)) as Objects[] value
  2448. // - if UsedValuesBits is not nil, only the corresponding bits set are added
  2449. // - can be used e.g. to populate a combo box as such:
  2450. // ! PTypeInfo(TypeInfo(TMyEnum))^.EnumBaseType^.AddCaptionStrings(ComboBox.Items);
  2451. procedure AddCaptionStrings(Strings: TStrings; UsedValuesBits: Pointer=nil);
  2452. /// retrieve all trimed element names as CSV
  2453. procedure GetEnumNameTrimedAll(var result: RawUTF8; const Prefix: RawUTF8='';
  2454. quotedValues: boolean=false);
  2455. /// get the corresponding enumeration ordinal value, from its name without
  2456. // its first lowercase chars ('Done' will find otDone e.g.)
  2457. // - return -1 if not found (don't use directly this value to avoid any GPF)
  2458. function GetEnumNameTrimedValue(const EnumName: ShortString): Integer; overload;
  2459. /// get the corresponding enumeration ordinal value, from its name without
  2460. // its first lowercase chars ('Done' will find otDone e.g.)
  2461. // - return -1 if not found (don't use directly this value to avoid any GPF)
  2462. function GetEnumNameTrimedValue(Value: PUTF8Char): Integer; overload;
  2463. /// compute how many bytes this type would use to be stored as a enumerate
  2464. function SizeInStorageAsEnum: Integer;
  2465. /// compute how many bytes this type would use to be stored as a set
  2466. function SizeInStorageAsSet: Integer;
  2467. /// store an enumeration value from its ordinal representation
  2468. // - copy SizeInStorageAsEnum bytes from Ordinal to Value pointer
  2469. procedure SetEnumFromOrdinal(out Value; Ordinal: Integer);
  2470. end;
  2471. {$ifdef FPC}
  2472. {$PACKRECORDS 1}
  2473. {$else}
  2474. {$A-}
  2475. {$endif}
  2476. { Delphi and FPC compiler use packed storage for this internal type }
  2477. TRecordField =
  2478. {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}
  2479. packed
  2480. {$endif FPC_REQUIRES_PROPER_ALIGNMENT}
  2481. record
  2482. TypeInfo: PPTypeInfo;
  2483. {$ifdef FPC}
  2484. Offset: SizeInt;
  2485. {$else}
  2486. Offset: Cardinal;
  2487. {$endif FPC}
  2488. end;
  2489. TRecordType =
  2490. {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}
  2491. packed
  2492. {$endif FPC_REQUIRES_PROPER_ALIGNMENT}
  2493. record
  2494. Size: cardinal;
  2495. Count: integer;
  2496. Fields: array[word] of TRecordField;
  2497. end;
  2498. PRecordField = ^TRecordField;
  2499. PRecordType = ^TRecordType;
  2500. {$ifdef FPC}
  2501. {$PACKRECORDS 1}
  2502. {$else}
  2503. {$A-}
  2504. {$endif}
  2505. { Delphi and FPC compiler use packed storage for this internal type }
  2506. /// a wrapper containing type information definition
  2507. // - user types defined as an alias don't have this type information:
  2508. // & type NewType = OldType;
  2509. // - user types defined as new types have this type information:
  2510. // & type NewType = type OldType;
  2511. {$ifndef ISDELPHI2010}
  2512. TTypeInfo = object
  2513. {$else}
  2514. TTypeInfo = record
  2515. {$endif}
  2516. /// the value type family
  2517. Kind: TTypeKind;
  2518. /// the declared name of the type ('String','Word','RawUnicode'...)
  2519. Name: ShortString;
  2520. /// get the class type information
  2521. function ClassType: PClassType; {$ifdef HASINLINE}inline;{$endif}
  2522. /// create an instance of the corresponding class
  2523. // - will call TObject.Create, or TSQLRecord.Create virtual constructor
  2524. // - will raise EParsingException if class cannot be constructed on the fly,
  2525. // e.g. for a plain TCollectionItem class
  2526. function ClassCreate: TObject;
  2527. /// get the SQL type of this Delphi class type
  2528. // - returns either sftObject, sftID, sftMany or sftUnknown
  2529. function ClassSQLFieldType: TSQLFieldType; {$ifdef HASINLINE}inline;{$endif}
  2530. /// get the number of published properties in this class
  2531. // - you can count the plain fields without any getter function, if you
  2532. // do need only the published properties corresponding to some value
  2533. // actually stored, and ignore e.g. any textual conversion
  2534. function ClassFieldCount(onlyWithoutGetter: boolean): integer;
  2535. /// for ordinal types, get the storage size and sign
  2536. function OrdType: TOrdType; {$ifdef HASINLINE}inline;{$endif}
  2537. /// for set types, get the type information of the corresponding enumeration
  2538. function SetEnumType: PEnumType;
  2539. /// for gloating point types, get the storage size and procision
  2540. function FloatType: TFloatType; {$ifdef HASINLINE}inline;{$endif}
  2541. /// get the SQL type of this Delphi type, as managed with the database driver
  2542. function GetSQLFieldType: TSQLFieldType;
  2543. /// fast and easy find if a class type inherits from a specific class type
  2544. function InheritsFrom(AClass: TClass): boolean;
  2545. /// get the enumeration type information
  2546. function EnumBaseType: PEnumType; {$ifdef HASINLINE}inline;{$endif}
  2547. /// get the record type information
  2548. function RecordType: PRecordType; {$ifdef HASINLINE}inline;{$endif}
  2549. /// get the dynamic array type information of the stored item
  2550. function DynArrayItemType(aDataSize: PInteger=nil): PTypeInfo;
  2551. {$ifdef HASINLINE}inline;{$endif}
  2552. /// get the dynamic array size (in bytes) of the stored item
  2553. function DynArrayItemSize: integer; {$ifdef HASINLINE}inline;{$endif}
  2554. /// recognize most used string types, returning their code page
  2555. // - will recognize TSQLRawBlob as the fake CP_SQLRAWBLOB code page
  2556. // - will return the exact code page since Delphi 2009, from RTTI
  2557. // - for non Unicode versions of Delphi, will recognize WinAnsiString as
  2558. // CODEPAGE_US, RawUnicode as CP_UTF16, RawByteString as CP_RAWBYTESTRING,
  2559. // AnsiString as 0, and any other type as RawUTF8
  2560. function AnsiStringCodePage: integer; {$ifdef HASCODEPAGE}inline;{$endif}
  2561. /// get the TGUID of a given interface type information
  2562. // - returns nil if this type is not an interface
  2563. function InterfaceGUID: PGUID;
  2564. /// get the unit name of a given interface type information
  2565. // - returns '' if this type is not an interface
  2566. function InterfaceUnitName: PShortString;
  2567. /// get the ancestor/parent of a given interface type information
  2568. // - returns nil if this type has no parent
  2569. function InterfaceAncestor: PTypeInfo;
  2570. /// get all ancestors/parents of a given interface type information
  2571. // - only ancestors with an associated TGUID would be added
  2572. // - if OnlyImplementedBy is not nil, only the interface explicitly
  2573. // implemented by this class would be added, and AncestorsImplementedEntry[]
  2574. // would contain the corresponding PInterfaceEntry values
  2575. procedure InterfaceAncestors(out Ancestors: PTypeInfoDynArray;
  2576. OnlyImplementedBy: TInterfacedObjectClass;
  2577. out AncestorsImplementedEntry: TPointerDynArray);
  2578. end;
  2579. {$ifdef FPC}
  2580. {$PACKRECORDS 1}
  2581. {$else}
  2582. {$A-}
  2583. {$endif}
  2584. { Delphi and FPC compiler use packed storage for this internal type }
  2585. /// a wrapper containing a property definition, with GetValue() and SetValue()
  2586. // functions for direct Delphi / UTF-8 SQL type mapping/conversion
  2587. // - handle byte, word, integer, cardinal, Int64 properties as INTEGER
  2588. // - handle boolean properties as INTEGER (0 is false, anything else is true)
  2589. // - handle enumeration properties as INTEGER, storing the ordinal value of the
  2590. // enumeration (i.e. starting at 0 for the first element)
  2591. // - handle enumerations set properties as INTEGER, each bit corresponding to
  2592. // an enumeration (therefore a set of up to 64 elements can be stored in such
  2593. // a field)
  2594. // - handle RawUTF8 properties as TEXT (UTF-8 encoded) - this is the preferred
  2595. // field type for storing some textual content in the ORM
  2596. // - handle WinAnsiString properties as TEXT (UTF-8 decoded in WinAnsi char set)
  2597. // - handle RawUnicode properties as TEXT (UTF-8 decoded as UTF-16 Win32 unicode)
  2598. // - handle Single, Double and Extended properties as FLOAT
  2599. // - handle TDateTime properties as ISO-8061 encoded TEXT
  2600. // - handle TTimeLog properties as properietary fast INTEGER date time
  2601. // - handle Currency property as FLOAT (safely converted to/from currency)
  2602. // - handle TSQLRecord descendant properties as INTEGER ROWID index to another record
  2603. // (warning: the value contains pointer(ROWID), not a valid object memory - you
  2604. // have to manually retrieve the record, using a integer(IDField) typecast)
  2605. // - handle TSQLRecordMany descendant properties as an "has many" instance (this
  2606. // is a particular case of TSQLRecord: it won't contain pointer(ID), but an object)
  2607. // - handle TRecordReference properties as INTEGER (64-bit) RecordRef-like value
  2608. // (use TSQLRest.Retrieve(Reference) to get a record content)
  2609. // - handle TSQLRawBlob properties as BLOB
  2610. // - handle dynamic arrays as BLOB, in the TDynArray.SaveTo binary format (is able
  2611. // to handle dynamic arrays of records, with records or strings within records)
  2612. // - handle records as BLOB, in the RecordSave binary format (our code is ready
  2613. // for that, but Delphi doesn't create the RTTI for records so it won't work)
  2614. // - WideString, shortstring, UnicodeString (i.e. Delphi 2009+ generic string),
  2615. // indexed properties are not handled yet (use faster RawUnicodeString instead
  2616. // of WideString and UnicodeString) - in fact, the generic string type is handled
  2617. {$ifndef ISDELPHI2010}
  2618. TPropInfo = object
  2619. protected
  2620. {$else}
  2621. TPropInfo = packed record
  2622. private
  2623. {$endif}
  2624. function GetOrdProp(Instance: TObject): PtrInt;
  2625. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2626. function GetObjProp(Instance: TObject): TObject;
  2627. {$ifdef HASINLINE}inline;{$endif}
  2628. procedure SetOrdProp(Instance: TObject; Value: PtrInt);
  2629. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2630. function GetInt64Prop(Instance: TObject): Int64;
  2631. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2632. procedure SetInt64Prop(Instance: TObject; const Value: Int64);
  2633. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2634. procedure GetLongStrProp(Instance: TObject; var Value: RawByteString);
  2635. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2636. procedure SetLongStrProp(Instance: TObject; const Value: RawByteString);
  2637. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2638. procedure CopyLongStrProp(Source,Dest: TObject);
  2639. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2640. procedure GetWideStrProp(Instance: TObject; var Value: WideString);
  2641. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2642. procedure SetWideStrProp(Instance: TObject; const Value: WideString);
  2643. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2644. {$ifdef HASVARUSTRING}
  2645. function GetUnicodeStrProp(Instance: TObject): UnicodeString;
  2646. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2647. procedure SetUnicodeStrProp(Instance: TObject; const Value: UnicodeString);
  2648. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2649. {$endif HASVARUSTRING}
  2650. function GetCurrencyProp(Instance: TObject): currency;
  2651. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2652. procedure SetCurrencyProp(Instance: TObject; const Value: Currency);
  2653. {$ifdef HASINLINE}inline;{$endif}
  2654. function GetDoubleProp(Instance: TObject): double;
  2655. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2656. procedure SetDoubleProp(Instance: TObject; Value: Double);
  2657. {$ifdef HASINLINE}inline;{$endif}
  2658. function GetFloatProp(Instance: TObject): double;
  2659. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2660. procedure SetFloatProp(Instance: TObject; Value: TSynExtended);
  2661. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2662. {$ifndef NOVARIANTS}
  2663. procedure GetVariantProp(Instance: TObject; var result: Variant);
  2664. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2665. procedure SetVariantProp(Instance: TObject; const Value: Variant);
  2666. {$ifdef USETYPEINFO}{$ifdef HASINLINE}inline;{$endif}{$endif}
  2667. {$endif}
  2668. public
  2669. /// the type definition of this property
  2670. PropType: PPTypeInfo;
  2671. /// contains the offset of a field, or the getter method set by 'read' Delphi declaration
  2672. GetProc: PtrInt;
  2673. /// contains the offset of a field, or the setter method set by 'write' Delphi declaration
  2674. // - if this field is nil (no 'write' was specified), SetValue() use GetProc to
  2675. // get the field memory address to save into
  2676. SetProc: PtrInt;
  2677. /// contains the 'stored' boolean value/method (used in TPersistent saving)
  2678. // - either integer(True) - the default, integer(False), reference to a Boolean
  2679. // field, or reference to a parameterless method that returns a Boolean value
  2680. // - if a property is marked as "stored AS_UNIQUE" (i.e. "stored false"),
  2681. // it is created as UNIQUE in the SQL database and its bit is set in
  2682. // Model.fIsUnique[]
  2683. StoredProc: PtrInt;
  2684. /// contains the index value of an indexed class data property
  2685. // - outside SQLite3, this can be used to define a VARCHAR() length value
  2686. // for the textual field definition (sftUTF8Text/sftAnsiText); e.g.
  2687. // the following will create a NAME VARCHAR(40) field:
  2688. // ! Name: RawUTF8 index 40 read fName write fName;
  2689. // - is used by a dynamic array property for fast usage of the
  2690. // TSQLRecord.DynArray(DynArrayFieldIndex) method
  2691. Index: Integer;
  2692. /// contains the default value (2147483648=$80000000 indicates nodefault)
  2693. // when an ordinal or set property is saved as TPersistent
  2694. Default: Longint;
  2695. /// index of the property in the current inherited class definition
  2696. // - first name index at a given class level is 0
  2697. // - index is reset to 0 at every inherited class level
  2698. NameIndex: SmallInt;
  2699. {$ifdef FPC}
  2700. /// contains the type of the GetProc/SetProc/StoredProc, see also ptxxx
  2701. // bit 0..1 GetProc e.g. PropProcs and 3=ptField
  2702. // 2..3 SetProc e.g. (PropProcs shr 2) and 3=ptField
  2703. // 4..5 StoredProc
  2704. // 6 : true, constant index property
  2705. PropProcs : Byte;
  2706. {$endif}
  2707. /// the property definition Name
  2708. Name: ShortString;
  2709. /// the type information of this property
  2710. // - would de-reference the PropType pointer on Delphi and newer FPC compilers
  2711. function TypeInfo: PTypeInfo;
  2712. {$ifdef HASINLINE}inline;{$endif}
  2713. /// get the next property information
  2714. // - no range check: use ClassProp()^.PropCount to determine the properties count
  2715. // - get the first PPropInfo with ClassProp()^.PropList
  2716. function Next: PPropInfo;
  2717. {$ifdef FPC}inline;{$else}{$ifdef HASINLINE}inline;{$endif} {$endif}
  2718. /// return FALSE (AS_UNIQUE) if was marked as "stored AS_UNIQUE"
  2719. // (i.e. "stored false"), or TRUE by default
  2720. // - if Instance=nil, will work only at RTTI level, not with field or method
  2721. // (and will return TRUE if nothing is defined in the RTTI)
  2722. function IsStored(Instance: TObject): boolean;
  2723. /// copy a published property value from one instance to another
  2724. // - this method use direct copy of the low-level binary content, and is
  2725. // therefore faster than a SetValue(Dest,GetValue(Source)) call
  2726. // - if DestInfo is nil, it will assume DestInfo=@self
  2727. procedure CopyValue(Source, Dest: TObject; DestInfo: PPropInfo=nil);
  2728. /// create a new instance of a published property
  2729. // - copying its properties values from a given instance of another class
  2730. // - if the destination property is not of the aFrom class, it will first
  2731. // search for any extact mach in the destination nested properties
  2732. function CopyToNewObject(aFrom: TObject): TObject;
  2733. /// compare two published properties
  2734. function SameValue(Source: TObject; DestInfo: PPropInfo; Dest: TObject): boolean;
  2735. /// return true if this property is a BLOB (TSQLRawBlob)
  2736. function IsBlob: boolean;
  2737. {$ifdef HASINLINE}inline;{$endif}
  2738. /// low-level getter of the ordinal property value of a given instance
  2739. // - this method will check if the corresponding property is ordinal
  2740. // - return -1 on any error
  2741. function GetOrdValue(Instance: TObject): PtrInt;
  2742. {$ifdef HASINLINE}inline;{$endif}
  2743. /// low-level getter of the ordinal property value of a given instance
  2744. // - this method will check if the corresponding property is ordinal
  2745. // - ordinal properties smaller than tkInt64 will return an Int64-converted
  2746. // value (e.g. tkInteger)
  2747. // - return 0 on any error
  2748. function GetInt64Value(Instance: TObject): Int64;
  2749. /// low-level getter of the currency property value of a given instance
  2750. // - this method will check if the corresponding property is exactly currency
  2751. // - return 0 on any error
  2752. function GetCurrencyValue(Instance: TObject): Currency;
  2753. /// low-level getter of the floating-point property value of a given instance
  2754. // - this method will check if the corresponding property is floating-point
  2755. // - return 0 on any error
  2756. function GetExtendedValue(Instance: TObject): TSynExtended;
  2757. /// low-level setter of the floating-point property value of a given instance
  2758. // - this method will check if the corresponding property is floating-point
  2759. procedure SetExtendedValue(Instance: TObject; const Value: TSynExtended);
  2760. /// low-level getter of the long string property value of a given instance
  2761. // - this method will check if the corresponding property is a Long String,
  2762. // and will return '' if it's not the case
  2763. // - it will convert the property content into RawUTF8, for RawUnicode,
  2764. // WinAnsiString, TSQLRawBlob and generic Delphi 6-2007 string property
  2765. // - WideString and UnicodeString properties will also be UTF-8 converted
  2766. procedure GetLongStrValue(Instance: TObject; var result: RawUTF8);
  2767. /// low-level getter of the long string property content of a given instance
  2768. // - just a wrapper around low-level GetLongStrProp() function
  2769. // - call GetLongStrValue() method if you want a conversion into RawUTF8
  2770. // - will work only for Kind=tkLString
  2771. procedure GetRawByteStringValue(Instance: TObject; var Value: RawByteString);
  2772. /// low-level setter of the ordinal property value of a given instance
  2773. // - this method will check if the corresponding property is ordinal
  2774. procedure SetOrdValue(Instance: TObject; Value: PtrInt);
  2775. /// low-level setter of the ordinal property value of a given instance
  2776. // - this method will check if the corresponding property is ordinal
  2777. procedure SetInt64Value(Instance: TObject; Value: Int64);
  2778. /// low-level setter of the long string property value of a given instance
  2779. // - this method will check if the corresponding property is a Long String
  2780. // - it will convert the property content into RawUTF8, for RawUnicode,
  2781. // WinAnsiString, TSQLRawBlob and generic Delphi 6-2007 string property
  2782. // - will set WideString and UnicodeString properties from UTF-8 content
  2783. procedure SetLongStrValue(Instance: TObject; const Value: RawUTF8);
  2784. /// low-level setter of the string property value of a given instance
  2785. // - uses the generic string type: to be used within the VCL
  2786. // - this method will check if the corresponding property is a Long String
  2787. // or an UnicodeString (for Delphi 2009+), and will call the corresponding
  2788. // SetLongStrValue() or SetUnicodeStrValue() method
  2789. procedure SetGenericStringValue(Instance: TObject; const Value: string);
  2790. /// low-level getter of the long string property value of a given instance
  2791. // - uses the generic string type: to be used within the VCL
  2792. // - this method will check if the corresponding property is a Long String,
  2793. // or an UnicodeString (for Delphi 2009+),and will return '' if it's
  2794. // not the case
  2795. function GetGenericStringValue(Instance: TObject): string;
  2796. {$ifdef HASVARUSTRING}
  2797. /// low-level setter of the Unicode string property value of a given instance
  2798. // - this method will check if the corresponding property is a Unicode String
  2799. procedure SetUnicodeStrValue(Instance: TObject; const Value: UnicodeString);
  2800. /// low-level getter of the Unicode string property value of a given instance
  2801. // - this method will check if the corresponding property is a Unicode String
  2802. function GetUnicodeStrValue(Instance: TObject): UnicodeString;
  2803. {$endif}
  2804. /// low-level getter of a dynamic array wrapper
  2805. // - this method will NOT check if the property is a dynamic array: caller
  2806. // must have already checked that PropType^^.Kind=tkDynArray
  2807. function GetDynArray(Instance: TObject): TDynArray; overload;
  2808. {$ifdef HASINLINE}inline;{$endif}
  2809. /// low-level getter of a dynamic array wrapper
  2810. // - this method will NOT check if the property is a dynamic array: caller
  2811. // must have already checked that PropType^^.Kind=tkDynArray
  2812. procedure GetDynArray(Instance: TObject; var result: TDynArray); overload;
  2813. {$ifdef HASINLINE}inline;{$endif}
  2814. /// return TRUE if this dynamic array has been registered as a T*ObjArray
  2815. // - the T*ObjArray dynamic array should have been previously registered
  2816. // via TJSONSerializer.RegisterObjArrayForJSON() overloaded methods
  2817. function DynArrayIsObjArray: boolean;
  2818. {$ifdef HASINLINE}inline;{$endif}
  2819. /// return class instance creation information about a T*ObjArray
  2820. // - the T*ObjArray dynamic array should have been previously registered
  2821. // via TJSONSerializer.RegisterObjArrayForJSON() overloaded methods
  2822. // - returns nil if the supplied type is not a registered T*ObjArray
  2823. // - you can create a new item instance just by calling result^.CreateNew
  2824. function DynArrayIsObjArrayInstance: PClassInstance;
  2825. {$ifdef HASINLINE}inline;{$endif}
  2826. /// return TRUE if the the property has no getter but direct field read
  2827. function GetterIsField: boolean;
  2828. {$ifdef HASINLINE}inline;{$endif}
  2829. /// return TRUE if the the property has no setter but direct field write
  2830. function SetterIsField: boolean;
  2831. {$ifdef HASINLINE}inline;{$endif}
  2832. /// return TRUE if the the property has a write setter or direct field
  2833. function WriteIsDefined: boolean;
  2834. {$ifdef HASINLINE}inline;{$endif}
  2835. /// returns the low-level field read address, if GetterIsField is TRUE
  2836. function GetterAddr(Instance: pointer): pointer;
  2837. {$ifdef HASINLINE}inline;{$endif}
  2838. /// returns the low-level field write address, if SetterIsField is TRUE
  2839. function SetterAddr(Instance: pointer): pointer;
  2840. {$ifdef HASINLINE}inline;{$endif}
  2841. /// low-level getter of the field value memory pointer
  2842. // - return NIL if both getter and setter are methods
  2843. function GetFieldAddr(Instance: TObject): pointer;
  2844. {$ifdef HASINLINE}inline;{$endif}
  2845. /// low-level setter of the property value as its default
  2846. // - this method will check the property type, e.g. setting '' for strings,
  2847. // and 0 for numbers, or running FreeAndNil() on any nested object (unless
  2848. // FreeAndNilNestedObjects is false so that ClearObject() is used
  2849. procedure SetDefaultValue(Instance: TObject; FreeAndNilNestedObjects: boolean=true);
  2850. {$ifndef NOVARIANTS}
  2851. /// low-level setter of the property value from a supplied variant
  2852. procedure SetFromVariant(Instance: TObject; const Value: variant);
  2853. {$endif NOVARIANTS}
  2854. /// read an TObject published property, as saved by ObjectToJSON() function
  2855. // - will use direct in-memory reference to the object, or call the corresponding
  2856. // setter method (if any), creating a temporary instance via TTypeInfo.ClassCreate
  2857. // - unserialize the JSON input buffer via a call to JSONToObject()
  2858. // - by default, a temporary instance would be created if a published field
  2859. // has a setter, and the instance is expected to be released later by the
  2860. // owner class: you can set the j2oSetterExpectsToFreeTempInstance option
  2861. // to let this method release it when the setter returns
  2862. function ClassFromJSON(Instance: TObject; From: PUTF8Char; var Valid: boolean;
  2863. Options: TJSONToObjectOptions=[]): PUTF8Char;
  2864. end;
  2865. {$ifdef FPC}
  2866. {$PACKRECORDS DEFAULT}
  2867. {$else}
  2868. {$A+}
  2869. {$endif}
  2870. /// the available methods calling conventions
  2871. // - this is by design only relevant to the x86 model
  2872. // - Win64 has one unique calling convention
  2873. TCallingConvention = (ccRegister, ccCdecl, ccPascal, ccStdCall, ccSafeCall);
  2874. /// the available kind of method parameters
  2875. TParamFlag = (pfVar, pfConst, pfArray, pfAddress, pfReference, pfOut,
  2876. {$ifdef FPC}pfConstRef{$else}pfResult{$endif});
  2877. /// a set of kind of method parameters
  2878. TParamFlags = set of TParamFlag;
  2879. PReturnInfo = ^TReturnInfo;
  2880. PCallingConvention = ^TCallingConvention;
  2881. PParamInfo = ^TParamInfo;
  2882. {$A-} { Delphi and FPC compiler use packed storage for this internal type }
  2883. /// a wrapper around method returned result definition
  2884. {$ifndef ISDELPHI2010}
  2885. TReturnInfo = object
  2886. {$else}
  2887. TReturnInfo = record
  2888. {$endif}
  2889. /// RTTI version
  2890. // - 2 up to Delphi 2010, 3 for Delphi XE and up
  2891. Version: byte;
  2892. /// expected calling convention (only relevant for x86 mode)
  2893. CallingConvention: TCallingConvention;
  2894. /// the expected type of the returned function result
  2895. // - is nil for procedure
  2896. ReturnType: ^PTypeInfo;
  2897. /// total size of data needed for stack parameters + 8 (ret-addr + pushed EBP)
  2898. ParamSize: Word;
  2899. /// number of expected parameters
  2900. ParamCount: Byte;
  2901. /// access to the first method parameter definition
  2902. function Param: PParamInfo;
  2903. {$ifdef HASINLINE}inline;{$endif}
  2904. end;
  2905. {$A-} { Delphi and FPC compiler use packed storage for this internal type }
  2906. /// a wrapper around an individual method parameter definition
  2907. {$ifndef ISDELPHI2010}
  2908. TParamInfo = object
  2909. {$else}
  2910. TParamInfo = record
  2911. {$endif}
  2912. /// the kind of parameter
  2913. Flags: TParamFlags;
  2914. /// the parameter type information
  2915. ParamType: PPTypeInfo;
  2916. {$ifdef FPC}
  2917. ParReg: byte;
  2918. Offset: longint;
  2919. {$else}
  2920. /// parameter offset
  2921. // - 0 for EAX, 1 for EDX, 2 for ECX
  2922. // - any value >= 8 for stack-based parameter
  2923. Offset: Word;
  2924. {$endif}
  2925. /// parameter name
  2926. Name: ShortString;
  2927. /// get the next parameter information
  2928. // - no range check: use TReturnInfo.ParamCount to determine the appropriate count
  2929. function Next: PParamInfo;
  2930. {$ifdef HASINLINE}inline;{$endif}
  2931. end;
  2932. {$A-} { Delphi and FPC compiler use packed storage for this internal type }
  2933. /// a wrapper around a method definition
  2934. {$ifndef ISDELPHI2010}
  2935. TMethodInfo = object
  2936. {$else}
  2937. TMethodInfo = record
  2938. {$endif}
  2939. {$ifdef FPC}
  2940. /// method name
  2941. Name: PShortString;
  2942. /// the associated method code address
  2943. Addr: Pointer;
  2944. {$else}
  2945. /// size (in bytes) of this TMethodInfo block
  2946. Len: Word;
  2947. /// the associated method code address
  2948. Addr: Pointer;
  2949. /// method name
  2950. Name: ShortString;
  2951. {$endif}
  2952. /// retrieve the associated parameters information
  2953. function ReturnInfo: PReturnInfo;
  2954. {$ifdef HASINLINE}inline;{$endif}
  2955. /// wrapper returning nil and avoiding a GPF if @self=nil
  2956. function MethodAddr: Pointer;
  2957. {$ifdef HASINLINE}inline;{$endif}
  2958. end;
  2959. {$ifdef FPC}
  2960. {$PACKRECORDS DEFAULT}
  2961. {$else}
  2962. {$A+} { default aligned data }
  2963. {$endif}
  2964. TJSONSerializer = class;
  2965. /// ORM attributes for a TSQLPropInfo definition
  2966. TSQLPropInfoAttribute = (
  2967. aIsUnique);
  2968. /// set of ORM attributes for a TSQLPropInfo definition
  2969. TSQLPropInfoAttributes = set of TSQLPropInfoAttribute;
  2970. /// abstract parent class to store information about a published property
  2971. // - property information could be retrieved from RTTI (TSQLPropInfoRTTI*),
  2972. // or be defined by code (TSQLPropInfoCustom derivated classes) when RTTI
  2973. // is not available
  2974. TSQLPropInfo = class
  2975. protected
  2976. fName: RawUTF8;
  2977. fNameUnflattened: RawUTF8;
  2978. fSQLFieldType: TSQLFieldType;
  2979. fSQLFieldTypeStored: TSQLFieldType;
  2980. fSQLDBFieldType: TSQLDBFieldType;
  2981. fAttributes: TSQLPropInfoAttributes;
  2982. fFieldWidth: integer;
  2983. fPropertyIndex: integer;
  2984. fFromRTTI: boolean;
  2985. function GetNameDisplay: string; virtual;
  2986. /// those two protected methods allow custom storage of binary content
  2987. // as text
  2988. // - default implementation is to use hexa (ToSQL=true) or Base64 encodings
  2989. procedure BinaryToText(var Value: RawUTF8; ToSQL: boolean; wasSQLString: PBoolean); virtual;
  2990. procedure TextToBinary(Value: PUTF8Char; var result: RawByteString); virtual;
  2991. function GetSQLFieldTypeName: PShortString;
  2992. function GetSQLFieldRTTITypeName: RawUTF8; virtual;
  2993. // overriden method shall use direct copy of the low-level binary content,
  2994. // to be faster than a DestInfo.SetValue(Dest,GetValue(Source)) call
  2995. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); virtual;
  2996. public
  2997. /// initialize the internal fields
  2998. // - should not be called directly, but with dedicated class methods like
  2999. // class function TSQLPropInfoRTTI.CreateFrom() or overridden constructors
  3000. constructor Create(const aName: RawUTF8; aSQLFieldType: TSQLFieldType;
  3001. aAttributes: TSQLPropInfoAttributes; aFieldWidth, aPropertyIndex: integer); reintroduce; virtual;
  3002. /// the property definition Name
  3003. property Name: RawUTF8 read fName;
  3004. /// the property definition Name, afer un-camelcase and translation
  3005. property NameDisplay: string read GetNameDisplay;
  3006. /// the property definition Name, with full path name if has been flattened
  3007. // - if the property has been flattened (for a TSQLPropInfoRTTI), the real
  3008. // full nested class will be returned, e.g. 'Address.Country.Iso' for
  3009. // the 'Address_Country' flattened property name
  3010. property NameUnflattened: RawUTF8 read fNameUnflattened;
  3011. /// the property index in the RTTI
  3012. property PropertyIndex: integer read fPropertyIndex;
  3013. /// the corresponding column type, as managed by the ORM layer
  3014. property SQLFieldType: TSQLFieldType read fSQLFieldType;
  3015. /// the corresponding column type, as stored by the ORM layer
  3016. // - match SQLFieldType, unless for SQLFieldType=sftNullable, in which this
  3017. // field would contain the simple type eventually stored in the database
  3018. property SQLFieldTypeStored: TSQLFieldType read fSQLFieldTypeStored;
  3019. /// the corresponding column type name, as managed by the ORM layer and
  3020. // retrieved by the RTTI
  3021. // - returns e.g. 'sftTimeLog'
  3022. property SQLFieldTypeName: PShortString read GetSQLFieldTypeName;
  3023. /// the type name, as defined in the RTTI
  3024. // - returns e.g. 'RawUTF8'
  3025. // - will return the TSQLPropInfo class name if it is not a TSQLPropInfoRTTI
  3026. property SQLFieldRTTITypeName: RawUTF8 read GetSQLFieldRTTITypeName;
  3027. /// the corresponding column type, as managed for abstract database access
  3028. // - TNullable* fields would report here the corresponding simple DB type,
  3029. // e.g. ftInt64 for TNullableInteger (following SQLFieldTypeStored value)
  3030. property SQLDBFieldType: TSQLDBFieldType read fSQLDBFieldType;
  3031. /// the corresponding column type name, as managed for abstract database access
  3032. function SQLDBFieldTypeName: PShortString;
  3033. /// the ORM attributes of this property
  3034. // - contains aIsUnique e.g for TSQLRecord published properties marked as
  3035. // ! property MyProperty: RawUTF8 stored AS_UNIQUE;
  3036. // (i.e. "stored false")
  3037. property Attributes: TSQLPropInfoAttributes read fAttributes;
  3038. /// the optional width of this field, in external databases
  3039. // - is set e.g. by index attribute of TSQLRecord published properties as
  3040. // ! property MyProperty: RawUTF8 index 10;
  3041. property FieldWidth: integer read fFieldWidth;
  3042. public
  3043. /// convert UTF-8 encoded text into the property value
  3044. // - setter method (write Set*) is called if available
  3045. // - if no setter exists (no write declaration), the getted field address is used
  3046. // - handle UTF-8 SQL to Delphi values conversion
  3047. // - expect BLOB fields encoded as SQlite3 BLOB literals ("x'01234'" e.g.)
  3048. // or base-64 encoded stream for JSON ("\uFFF0base64encodedbinary") - i.e.
  3049. // both format supported by BlobToTSQLRawBlob() function
  3050. // - handle TPersistent, TCollection, TRawUTF8List or TStrings with JSONToObject
  3051. // - note that the supplied Value buffer won't be modified by this method:
  3052. // overriden implementation should create their own temporary copy
  3053. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); virtual; abstract;
  3054. /// convert UTF-8 encoded text into the property value
  3055. // - just a wrapper around SetValue(...,pointer(Value),...) which may be
  3056. // optimized for overriden methods
  3057. procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); virtual;
  3058. /// convert the property value into an UTF-8 encoded text
  3059. // - if ToSQL is true, result is on SQL form (false->'0' e.g.)
  3060. // - if ToSQL is false, result is on JSON form (false->'false' e.g.)
  3061. // - BLOB field returns SQlite3 BLOB literals ("x'01234'" e.g.) if ToSQL is
  3062. // true, or base-64 encoded stream for JSON ("\uFFF0base64encodedbinary")
  3063. // - getter method (read Get*) is called if available
  3064. // - handle Delphi values into UTF-8 SQL conversion
  3065. // - sftBlobDynArray, sftBlobCustom or sftBlobRecord are returned as BLOB
  3066. // litterals ("X'53514C697465'") if ToSQL is true, or base-64 encoded stream
  3067. // for JSON ("\uFFF0base64encodedbinary")
  3068. // - handle TPersistent, TCollection, TRawUTF8List or TStrings with ObjectToJSON
  3069. function GetValue(Instance: TObject; ToSQL: boolean; wasSQLString: PBoolean=nil): RawUTF8;
  3070. {$ifdef HASINLINE}inline;{$endif}
  3071. /// convert the property value into an UTF-8 encoded text
  3072. // - this method is the same as GetValue(), but avoid assigning the result
  3073. // string variable (some speed up on multi-core CPUs, since avoid a CPU LOCK)
  3074. // - this virtual method is the one to be overridden by the implementing classes
  3075. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3076. var result: RawUTF8; wasSQLString: PBoolean); virtual; abstract;
  3077. /// normalize the content of Value, so that GetValue(Object,true) should return the
  3078. // same content (true for ToSQL format)
  3079. procedure NormalizeValue(var Value: RawUTF8); virtual; abstract;
  3080. /// retrieve a field value into a TSQLVar value
  3081. // - the temp RawByteString is used as a temporary storage for TEXT or BLOB
  3082. // and should be available during all access to the TSQLVar fields
  3083. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3084. var temp: RawByteString); virtual;
  3085. /// set a field value from a TSQLVar value
  3086. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; virtual;
  3087. /// append the property value into a binary buffer
  3088. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); virtual; abstract;
  3089. /// read the property value from a binary buffer
  3090. // - returns next char in input buffer on success, or nil in case of invalid
  3091. // content supplied e.g.
  3092. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; virtual; abstract;
  3093. /// copy a property value from one instance to another
  3094. // - both objects should have the same exact property
  3095. procedure CopyValue(Source, Dest: TObject); virtual;
  3096. /// copy a value from one instance to another property instance
  3097. // - if the property has been flattened (for a TSQLPropInfoRTTI), the real
  3098. // Source/Dest instance will be used for the copy
  3099. procedure CopyProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject);
  3100. {$ifndef NOVARIANTS}
  3101. /// retrieve the property value into a Variant
  3102. // - will set the Variant type to the best matching kind according to the
  3103. // SQLFieldType type
  3104. // - BLOB field returns SQlite3 BLOB textual literals ("x'01234'" e.g.)
  3105. // - dynamic array field is returned as a variant array
  3106. procedure GetVariant(Instance: TObject; var Dest: Variant); virtual;
  3107. /// set the property value from a Variant value
  3108. // - dynamic array field must be set from a variant array
  3109. // - will set the Variant type to the best matching kind according to the
  3110. // SQLFieldType type
  3111. // - expect BLOB fields encoded as SQlite3 BLOB literals ("x'01234'" e.g.)
  3112. procedure SetVariant(Instance: TObject; const Source: Variant); virtual;
  3113. {$endif}
  3114. /// compare the content of the property of two objects
  3115. // - not all kind of properties are handled: only main types (like GetHash)
  3116. // - if CaseInsensitive is TRUE, will apply NormToUpper[] 8 bits uppercase,
  3117. // handling RawUTF8 properties just like the SYSTEMNOCASE collation
  3118. // - this method should match the case-sensitivity of GetHash()
  3119. // - this default implementation will call GetValueVar() for slow comparison
  3120. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; virtual;
  3121. /// retrieve an unsigned 32 bit hash of the corresponding property
  3122. // - not all kind of properties are handled: only main types
  3123. // - if CaseInsensitive is TRUE, will apply NormToUpper[] 8 bits uppercase,
  3124. // handling RawUTF8 properties just like the SYSTEMNOCASE collation
  3125. // - note that this method can return a hash value of 0
  3126. // - this method should match the case-sensitivity of CompareValue()
  3127. // - this default implementation will call GetValueVar() for slow computation
  3128. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; virtual;
  3129. /// add the JSON content corresponding to the given property
  3130. // - this default implementation will call safe but slow GetValueVar() method
  3131. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); virtual;
  3132. /// returns an untyped pointer to the field property memory in a given instance
  3133. function GetFieldAddr(Instance: TObject): pointer; virtual; abstract;
  3134. end;
  3135. /// class-reference type (metaclass) of a TSQLPropInfo information
  3136. TSQLPropInfoClass = class of TSQLPropInfo;
  3137. /// define how the published properties RTTI is to be interpreted
  3138. // - i.e. how TSQLPropInfoList.Create() and TSQLPropInfoRTTI.CreateFrom()
  3139. // would handle the incoming RTTI
  3140. TSQLPropInfoListOptions = set of (
  3141. pilRaiseEORMExceptionIfNotHandled, pilAllowIDFields,
  3142. pilSubClassesFlattening, pilIgnoreIfGetter,
  3143. pilSingleHierarchyLevel);
  3144. /// parent information about a published property retrieved from RTTI
  3145. TSQLPropInfoRTTI = class(TSQLPropInfo)
  3146. protected
  3147. fPropInfo: PPropInfo;
  3148. fPropType: PTypeInfo;
  3149. fFlattenedProps: PPropInfoDynArray;
  3150. fGetterIsFieldPropOffset: cardinal;
  3151. fInPlaceCopySameClassPropOffset: cardinal;
  3152. function GetSQLFieldRTTITypeName: RawUTF8; override;
  3153. public
  3154. /// this meta-constructor will create an instance of the exact descendant
  3155. // of the specified property RTTI
  3156. // - it will raise an EORMException in case of an unhandled type
  3157. class function CreateFrom(aPropInfo: PPropInfo; aPropIndex: integer;
  3158. aOptions: TSQLPropInfoListOptions; const aFlattenedProps: PPropInfoDynArray): TSQLPropInfo;
  3159. /// initialize the internal fields
  3160. // - should not be called directly, but with dedicated class methods like
  3161. // class function CreateFrom()
  3162. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer;
  3163. aSQLFieldType: TSQLFieldType); reintroduce; virtual;
  3164. {$ifndef NOVARIANTS}
  3165. /// retrieve the property value into a Variant
  3166. // - will set the Variant type to the best matching kind according to the
  3167. // SQLFieldType type
  3168. // - BLOB field returns SQlite3 BLOB textual literals ("x'01234'" e.g.)
  3169. // - dynamic array field is returned as a variant array
  3170. procedure GetVariant(Instance: TObject; var Dest: Variant); override;
  3171. {$endif}
  3172. /// generic way of implementing it
  3173. function GetFieldAddr(Instance: TObject): pointer; override;
  3174. /// for pilSubClassesFlattening properties, compute the actual instance
  3175. // containing the property value
  3176. // - if the property was not flattened, return the instance
  3177. function Flattened(Instance: TObject): TObject;
  3178. /// corresponding RTTI information
  3179. property PropInfo: PPropInfo read fPropInfo;
  3180. /// for pilSubClassesFlattening properties, the parents RTTI
  3181. property FlattenedPropInfo: PPropInfoDynArray read fFlattenedProps;
  3182. /// corresponding type information, as retrieved from PropInfo RTTI
  3183. property PropType: PTypeInfo read fPropType;
  3184. end;
  3185. /// class-reference type (metaclass) of a TSQLPropInfoRTTI information
  3186. TSQLPropInfoRTTIClass = class of TSQLPropInfoRTTI;
  3187. TSQLPropInfoRTTIObjArray = array of TSQLPropInfoRTTI;
  3188. /// information about an ordinal Int32 published property
  3189. TSQLPropInfoRTTIInt32 = class(TSQLPropInfoRTTI)
  3190. protected
  3191. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3192. public
  3193. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3194. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3195. var result: RawUTF8; wasSQLString: PBoolean); override;
  3196. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3197. var temp: RawByteString); override;
  3198. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3199. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3200. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3201. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3202. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3203. procedure NormalizeValue(var Value: RawUTF8); override;
  3204. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3205. end;
  3206. /// information about a set published property
  3207. TSQLPropInfoRTTISet = class(TSQLPropInfoRTTIInt32)
  3208. protected
  3209. fSetEnumType: PEnumType;
  3210. public
  3211. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType); override;
  3212. property SetEnumType: PEnumType read fSetEnumType;
  3213. end;
  3214. /// information about a enumeration published property
  3215. // - can be either sftBoolean or sftEnumerate kind of property
  3216. TSQLPropInfoRTTIEnum = class(TSQLPropInfoRTTIInt32)
  3217. protected
  3218. fEnumType: PEnumType;
  3219. public
  3220. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType); override;
  3221. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3222. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3223. var result: RawUTF8; wasSQLString: PBoolean); override;
  3224. procedure NormalizeValue(var Value: RawUTF8); override;
  3225. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3226. function GetCaption(Value: RawUTF8; out IntValue: integer): string;
  3227. property EnumType: PEnumType read fEnumType;
  3228. end;
  3229. /// information about a character published property
  3230. TSQLPropInfoRTTIChar = class(TSQLPropInfoRTTIInt32)
  3231. public
  3232. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3233. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3234. var result: RawUTF8; wasSQLString: PBoolean); override;
  3235. procedure NormalizeValue(var Value: RawUTF8); override;
  3236. end;
  3237. /// information about an ordinal Int64 published property
  3238. TSQLPropInfoRTTIInt64 = class(TSQLPropInfoRTTI)
  3239. protected
  3240. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3241. public
  3242. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3243. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3244. var result: RawUTF8; wasSQLString: PBoolean); override;
  3245. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3246. var temp: RawByteString); override;
  3247. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3248. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3249. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3250. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3251. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3252. procedure NormalizeValue(var Value: RawUTF8); override;
  3253. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3254. end;
  3255. /// information about a TTimeLog published property
  3256. // - stored as an Int64, but with a specific class
  3257. TSQLPropInfoRTTITimeLog = class(TSQLPropInfoRTTIInt64);
  3258. /// information about a floating-point Double published property
  3259. TSQLPropInfoRTTIDouble = class(TSQLPropInfoRTTI)
  3260. protected
  3261. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3262. public
  3263. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3264. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3265. var result: RawUTF8; wasSQLString: PBoolean); override;
  3266. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3267. var temp: RawByteString); override;
  3268. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3269. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3270. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3271. procedure NormalizeValue(var Value: RawUTF8); override;
  3272. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3273. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3274. end;
  3275. /// information about a fixed-decimal Currency published property
  3276. TSQLPropInfoRTTICurrency = class(TSQLPropInfoRTTIDouble)
  3277. protected
  3278. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3279. public
  3280. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3281. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3282. var result: RawUTF8; wasSQLString: PBoolean); override;
  3283. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3284. var temp: RawByteString); override;
  3285. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3286. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3287. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3288. procedure NormalizeValue(var Value: RawUTF8); override;
  3289. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3290. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3291. end;
  3292. /// information about a TDateTime published property
  3293. TSQLPropInfoRTTIDateTime = class(TSQLPropInfoRTTIDouble)
  3294. public
  3295. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3296. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3297. var result: RawUTF8; wasSQLString: PBoolean); override;
  3298. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3299. var temp: RawByteString); override;
  3300. procedure NormalizeValue(var Value: RawUTF8); override;
  3301. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3302. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3303. end;
  3304. /// information about a AnsiString published property
  3305. TSQLPropInfoRTTIAnsi = class(TSQLPropInfoRTTI)
  3306. protected
  3307. fEngine: TSynAnsiConvert;
  3308. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3309. public
  3310. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType); override;
  3311. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3312. procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override;
  3313. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3314. var result: RawUTF8; wasSQLString: PBoolean); override;
  3315. procedure CopyValue(Source, Dest: TObject); override;
  3316. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3317. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3318. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3319. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3320. var temp: RawByteString); override;
  3321. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3322. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3323. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3324. procedure NormalizeValue(var Value: RawUTF8); override;
  3325. end;
  3326. /// information about a RawUTF8 published property
  3327. TSQLPropInfoRTTIRawUTF8 = class(TSQLPropInfoRTTIAnsi)
  3328. protected
  3329. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3330. public
  3331. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3332. procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override;
  3333. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3334. var result: RawUTF8; wasSQLString: PBoolean); override;
  3335. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3336. var temp: RawByteString); override;
  3337. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3338. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3339. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3340. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3341. end;
  3342. /// information about a RawUnicode published property
  3343. TSQLPropInfoRTTIRawUnicode = class(TSQLPropInfoRTTIAnsi)
  3344. protected
  3345. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3346. public
  3347. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3348. procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override;
  3349. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3350. var result: RawUTF8; wasSQLString: PBoolean); override;
  3351. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3352. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3353. end;
  3354. /// information about a TSQLRawBlob published property
  3355. TSQLPropInfoRTTIRawBlob = class(TSQLPropInfoRTTIAnsi)
  3356. protected
  3357. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3358. public
  3359. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3360. procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override;
  3361. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3362. var result: RawUTF8; wasSQLString: PBoolean); override;
  3363. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3364. var temp: RawByteString); override;
  3365. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3366. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3367. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3368. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3369. procedure GetBlob(Instance: TObject; var Blob: RawByteString);
  3370. procedure SetBlob(Instance: TObject; const Blob: RawByteString);
  3371. function IsNull(Instance: TObject): Boolean;
  3372. end;
  3373. /// information about a WideString published property
  3374. TSQLPropInfoRTTIWide = class(TSQLPropInfoRTTI)
  3375. protected
  3376. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3377. public
  3378. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3379. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3380. var result: RawUTF8; wasSQLString: PBoolean); override;
  3381. procedure CopyValue(Source, Dest: TObject); override;
  3382. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3383. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3384. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3385. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3386. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3387. end;
  3388. {$ifdef HASVARUSTRING}
  3389. /// information about a UnicodeString published property
  3390. TSQLPropInfoRTTIUnicode = class(TSQLPropInfoRTTI)
  3391. protected
  3392. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3393. public
  3394. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3395. procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override;
  3396. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3397. var result: RawUTF8; wasSQLString: PBoolean); override;
  3398. procedure CopyValue(Source, Dest: TObject); override;
  3399. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3400. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3401. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3402. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3403. var temp: RawByteString); override;
  3404. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3405. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3406. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3407. end;
  3408. {$endif HASVARUSTRING}
  3409. /// information about a dynamic array published property
  3410. TSQLPropInfoRTTIDynArray = class(TSQLPropInfoRTTI)
  3411. protected
  3412. fObjArray: PClassInstance;
  3413. function GetDynArray(Instance: TObject): TDynArray; overload;
  3414. {$ifdef HASINLINE}inline;{$endif}
  3415. procedure GetDynArray(Instance: TObject; var result: TDynArray); overload;
  3416. {$ifdef HASINLINE}inline;{$endif}
  3417. function GetDynArrayElemType: PTypeInfo;
  3418. /// will create TDynArray.SaveTo by default, or JSON if is T*ObjArray
  3419. procedure Serialize(Instance: TObject; var data: RawByteString; ExtendedJson: boolean); virtual;
  3420. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3421. public
  3422. /// initialize the internal fields
  3423. // - should not be called directly, but with dedicated class methods like
  3424. // class function CreateFrom()
  3425. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer;
  3426. aSQLFieldType: TSQLFieldType); override;
  3427. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3428. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3429. var result: RawUTF8; wasSQLString: PBoolean); override;
  3430. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3431. var temp: RawByteString); override;
  3432. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3433. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3434. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3435. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3436. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3437. procedure NormalizeValue(var Value: RawUTF8); override;
  3438. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3439. {$ifndef NOVARIANTS}
  3440. procedure GetVariant(Instance: TObject; var Dest: Variant); override;
  3441. procedure SetVariant(Instance: TObject; const Source: Variant); override;
  3442. {$endif}
  3443. /// optional index of the dynamic array published property
  3444. // - used e.g. for fast lookup by TSQLRecord.DynArray(DynArrayFieldIndex)
  3445. property DynArrayIndex: integer read fFieldWidth;
  3446. /// read-only access to the low-level type information the array item type
  3447. property DynArrayElemType: PTypeInfo read GetDynArrayElemType;
  3448. /// dynamic array item information for a T*ObjArray
  3449. // - equals nil if this dynamic array was not previously registered via
  3450. // TJSONSerializer.RegisterObjArrayForJSON()
  3451. // - note that if the field is a T*ObjArray, you could create a new item
  3452. // by calling ObjArray^.CreateNew
  3453. // - T*ObjArray database column will be stored as text
  3454. property ObjArray: PClassInstance read fObjArray;
  3455. end;
  3456. TSQLPropInfoRTTIDynArrayObjArray = array of TSQLPropInfoRTTIDynArray;
  3457. {$ifndef NOVARIANTS}
  3458. /// information about a variant published property
  3459. // - is also used for TNullable* properties
  3460. TSQLPropInfoRTTIVariant = class(TSQLPropInfoRTTI)
  3461. protected
  3462. fDocVariantOptions: TDocVariantOptions;
  3463. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3464. public
  3465. /// initialize the internal fields
  3466. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer;
  3467. aSQLFieldType: TSQLFieldType); override;
  3468. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3469. procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override;
  3470. procedure SetValuePtr(Instance: TObject; Value: PUTF8Char; ValueLen: integer;
  3471. wasString: boolean);
  3472. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3473. var result: RawUTF8; wasSQLString: PBoolean); override;
  3474. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3475. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3476. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3477. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3478. procedure NormalizeValue(var Value: RawUTF8); override;
  3479. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3480. procedure GetVariant(Instance: TObject; var Dest: Variant); override;
  3481. procedure SetVariant(Instance: TObject; const Source: Variant); override;
  3482. /// how this property will deal with its instances (including TDocVariant)
  3483. // - by default, contains JSON_OPTIONS_FAST for best performance - i.e.
  3484. // [dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]
  3485. // - set JSON_OPTIONS_FAST_EXTENDED (or include dvoSerializeAsExtendedJson)
  3486. // so that any TDocVariant nested field names would not be double-quoted,
  3487. // saving some chars in the stored TEXT column and in the JSON escaped
  3488. // transmitted data over REST, by writing '{name:"John",age:123}' instead of
  3489. // '{"name":"John","age":123}': be aware that this syntax is supported by
  3490. // the ORM, SOA, TDocVariant, TBSONVariant, and our SynCrossPlatformJSON
  3491. // unit, but not AJAX/JavaScript or most JSON libraries
  3492. // - see also TSQLModel/TSQLRecordProperties.SetVariantFieldsDocVariantOptions
  3493. property DocVariantOptions: TDocVariantOptions read fDocVariantOptions write fDocVariantOptions;
  3494. end;
  3495. {$endif NOVARIANTS}
  3496. /// optional event handler used by TSQLPropInfoRecord to handle textual storage
  3497. // - by default, TSQLPropInfoRecord content will be stored as sftBlobCustom;
  3498. // specify such a callback event to allow storage as UTF-8 textual field and
  3499. // use a sftUTF8Custom kind of column
  3500. // - event implementation shall convert data/datalen binary value into Text
  3501. TOnSQLPropInfoRecord2Text = procedure(Data: pointer; DataLen: integer;
  3502. var Text: RawUTF8);
  3503. /// optional event handler used by TSQLPropInfoRecord to handle textual storage
  3504. // - by default, TSQLPropInfoRecord content will be stored as sftBlobCustom;
  3505. // specify such a callback event to allow storage as UTF-8 textual field and
  3506. // use a sftUTF8Custom kind of column
  3507. // - event implementaiton shall convert Text into Data binary value
  3508. TOnSQLPropInfoRecord2Data = procedure(Text: PUTF8Char; var Data: RawByteString);
  3509. /// abstract information about a record-like property defined directly in code
  3510. // - do not use this class, but TSQLPropInfoRecordRTTI and TSQLPropInfoRecordFixedSize
  3511. // - will store the content as BLOB by default, and SQLFieldType as sftBlobCustom
  3512. // - if aData2Text/aText2Data are defined, use TEXT storage and sftUTF8Custom type
  3513. TSQLPropInfoCustom = class(TSQLPropInfo)
  3514. protected
  3515. fOffset: PtrUInt;
  3516. fData2Text: TOnSQLPropInfoRecord2Text;
  3517. fText2Data: TOnSQLPropInfoRecord2Data;
  3518. procedure BinaryToText(var Value: RawUTF8; ToSQL: boolean; wasSQLString: PBoolean); override;
  3519. procedure TextToBinary(Value: PUTF8Char; var result: RawByteString); override;
  3520. public
  3521. /// define a custom property in code
  3522. // - do not call this constructor directly, but one of its inherited classes,
  3523. // via a call to TSQLRecordProperties.RegisterCustom*()
  3524. constructor Create(const aName: RawUTF8; aSQLFieldType: TSQLFieldType;
  3525. aAttributes: TSQLPropInfoAttributes; aFieldWidth, aPropIndex: Integer;
  3526. aProperty: pointer; aData2Text: TOnSQLPropInfoRecord2Text;
  3527. aText2Data: TOnSQLPropInfoRecord2Data); reintroduce;
  3528. public
  3529. function GetFieldAddr(Instance: TObject): pointer; override;
  3530. end;
  3531. /// information about a record property defined directly in code using RTTI
  3532. TSQLPropInfoRecordTyped = class(TSQLPropInfoCustom)
  3533. protected
  3534. fTypeInfo: PTypeInfo;
  3535. public
  3536. property TypeInfo: PTypeInfo read fTypeInfo;
  3537. end;
  3538. /// information about a record property defined directly in code
  3539. // - Delphi does not publish RTTI for published record properties
  3540. // - you can use this class to register a record property from its RTTI
  3541. // - will store the content as BLOB by default, and SQLFieldType as sftBlobCustom
  3542. // - if aData2Text/aText2Data are defined, use TEXT storage and sftUTF8Custom type
  3543. // - this class will use only binary RecordLoad/RecordSave methods
  3544. TSQLPropInfoRecordRTTI = class(TSQLPropInfoRecordTyped)
  3545. protected
  3546. function GetSQLFieldRTTITypeName: RawUTF8; override;
  3547. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3548. public
  3549. /// define a record property from its RTTI definition
  3550. // - handle any kind of record with available generated TypeInfo()
  3551. // - aPropertyPointer shall be filled with the offset to the private
  3552. // field within a nil object, e.g for
  3553. // ! class TMainObject = class(TSQLRecord)
  3554. // ! (...)
  3555. // ! fFieldName: TMyRecord;
  3556. // ! public
  3557. // ! (...)
  3558. // ! property FieldName: TMyRecord read fFieldName write fFieldName;
  3559. // ! end;
  3560. // you will have to register it via a call to
  3561. // TSQLRecordProperties.RegisterCustomRTTIRecordProperty()
  3562. // - optional aIsNotUnique parametercanl be defined
  3563. // - implementation will use internally RecordLoad/RecordSave functions
  3564. // - you can specify optional aData2Text/aText2Data callbacks to store
  3565. // the content as textual values, and not as BLOB
  3566. constructor Create(aRecordInfo: PTypeInfo; const aName: RawUTF8; aPropertyIndex: integer;
  3567. aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[];
  3568. aFieldWidth: integer=0; aData2Text: TOnSQLPropInfoRecord2Text=nil;
  3569. aText2Data: TOnSQLPropInfoRecord2Data=nil); reintroduce; overload;
  3570. public
  3571. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3572. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3573. var result: RawUTF8; wasSQLString: PBoolean); override;
  3574. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3575. var temp: RawByteString); override;
  3576. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3577. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3578. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3579. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3580. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3581. procedure NormalizeValue(var Value: RawUTF8); override;
  3582. {$ifndef NOVARIANTS}
  3583. procedure GetVariant(Instance: TObject; var Dest: Variant); override;
  3584. procedure SetVariant(Instance: TObject; const Source: Variant); override;
  3585. {$endif}
  3586. end;
  3587. /// information about a fixed-size record property defined directly in code
  3588. // - Delphi does not publish RTTI for published record properties
  3589. // - you can use this class to register a record property with no RTTI (i.e.
  3590. // a record with no reference-counted types within)
  3591. // - will store the content as BLOB by default, and SQLFieldType as sftBlobCustom
  3592. // - if aData2Text/aText2Data are defined, use TEXT storage and sftUTF8Custom type
  3593. TSQLPropInfoRecordFixedSize = class(TSQLPropInfoRecordTyped)
  3594. protected
  3595. fRecordSize: integer;
  3596. function GetSQLFieldRTTITypeName: RawUTF8; override;
  3597. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  3598. public
  3599. /// define an unmanaged fixed-size record property
  3600. // - simple kind of records (i.e. those not containing reference-counted
  3601. // members) do not have RTTI generated, at least in older versions of Delphi:
  3602. // use this constructor to define a direct property access
  3603. // - main parameter is the record size, in bytes
  3604. constructor Create(aRecordSize: cardinal; const aName: RawUTF8; aPropertyIndex: integer;
  3605. aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[];
  3606. aFieldWidth: integer=0; aData2Text: TOnSQLPropInfoRecord2Text=nil;
  3607. aText2Data: TOnSQLPropInfoRecord2Data=nil); reintroduce; overload;
  3608. public
  3609. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3610. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3611. var result: RawUTF8; wasSQLString: PBoolean); override;
  3612. procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  3613. var temp: RawByteString); override;
  3614. function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override;
  3615. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3616. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3617. function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override;
  3618. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  3619. procedure NormalizeValue(var Value: RawUTF8); override;
  3620. {$ifndef NOVARIANTS}
  3621. procedure GetVariant(Instance: TObject; var Dest: Variant); override;
  3622. procedure SetVariant(Instance: TObject; const Source: Variant); override;
  3623. {$endif}
  3624. end;
  3625. /// information about a custom property defined directly in code
  3626. // - you can define any kind of property, either a record or any type
  3627. // - this class will use JSON serialization, by type name or TypeInfo() pointer
  3628. // - will store the content as TEXT by default, and SQLFieldType as sftUTF8Custom
  3629. TSQLPropInfoCustomJSON = class(TSQLPropInfoRecordTyped)
  3630. protected
  3631. fCustomParser: TJSONCustomParserRTTI;
  3632. function GetSQLFieldRTTITypeName: RawUTF8; override;
  3633. procedure SetCustomParser(aCustomParser: TJSONCustomParserRTTI);
  3634. public
  3635. /// initialize the internal fields
  3636. // - should not be called directly
  3637. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer);
  3638. reintroduce; overload; virtual;
  3639. /// define a custom property from its RTTI definition
  3640. // - handle any kind of property, e.g. from enhanced RTTI or a custom record
  3641. // defined via TTextWriter.RegisterCustomJSONSerializer[FromText]()
  3642. // - aPropertyPointer shall be filled with the offset to the private
  3643. // field within a nil object, e.g for
  3644. // ! class TMainObject = class(TSQLRecord)
  3645. // ! (...)
  3646. // ! fFieldName: TMyRecord;
  3647. // ! public
  3648. // ! (...)
  3649. // ! property FieldName: TMyRecord read fFieldName write fFieldName;
  3650. // ! end;
  3651. // you will have to register it via a call to
  3652. // TSQLRecordProperties.RegisterCustomPropertyFromRTTI()
  3653. // - optional aIsNotUnique parameter can be defined
  3654. // - implementation will use internally RecordLoadJSON/RecordSave functions
  3655. // - you can specify optional aData2Text/aText2Data callbacks to store
  3656. // the content as textual values, and not as BLOB
  3657. constructor Create(aTypeInfo: PTypeInfo; const aName: RawUTF8;
  3658. aPropertyIndex: integer; aPropertyPointer: pointer;
  3659. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0);
  3660. reintroduce; overload;
  3661. /// define a custom property from its RTTI definition
  3662. // - handle any kind of property, e.g. from enhanced RTTI or a custom record
  3663. // defined via TTextWriter.RegisterCustomJSONSerializer[FromText]()
  3664. // - aPropertyPointer shall be filled with the offset to the private
  3665. // field within a nil object, e.g for
  3666. // ! class TMainObject = class(TSQLRecord)
  3667. // ! (...)
  3668. // ! fGUID: TGUID;
  3669. // ! public
  3670. // ! (...)
  3671. // ! property GUID: TGUID read fGUID write fGUID;
  3672. // ! end;
  3673. // you will have to register it via a call to
  3674. // TSQLRecordProperties.RegisterCustomPropertyFromTypeName()
  3675. // - optional aIsNotUnique parameter can be defined
  3676. // - implementation will use internally RecordLoadJSON/RecordSave functions
  3677. // - you can specify optional aData2Text/aText2Data callbacks to store
  3678. // the content as textual values, and not as BLOB
  3679. constructor Create(const aTypeName, aName: RawUTF8;
  3680. aPropertyIndex: integer; aPropertyPointer: pointer;
  3681. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0);
  3682. reintroduce; overload;
  3683. /// finalize the instance
  3684. destructor Destroy; override;
  3685. /// the corresponding custom JSON parser
  3686. property CustomParser: TJSONCustomParserRTTI read fCustomParser;
  3687. public
  3688. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  3689. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  3690. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  3691. var result: RawUTF8; wasSQLString: PBoolean); override;
  3692. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  3693. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  3694. procedure NormalizeValue(var Value: RawUTF8); override;
  3695. end;
  3696. /// dynamic array of ORM fields information for published properties
  3697. TSQLPropInfoObjArray = array of TSQLPropInfo;
  3698. /// handle a read-only list of fields information for published properties
  3699. // - is mainly used by our ORM for TSQLRecord RTTI, but may be used for
  3700. // any TPersistent
  3701. TSQLPropInfoList = class
  3702. protected
  3703. fList: TSQLPropInfoObjArray;
  3704. fCount: integer;
  3705. fTable: TClass;
  3706. fOptions: TSQLPropInfoListOptions;
  3707. fOrderedByName: TIntegerDynArray;
  3708. function GetItem(aIndex: integer): TSQLPropInfo;
  3709. procedure QuickSortByName(L,R: PtrInt);
  3710. procedure InternalAddParentsFirst(aClassType: TClass); overload;
  3711. procedure InternalAddParentsFirst(aClassType: TClass;
  3712. aFlattenedProps: PPropInfoDynArray); overload;
  3713. public
  3714. /// initialize the list from a given class RTTI
  3715. constructor Create(aTable: TClass; aOptions: TSQLPropInfoListOptions);
  3716. /// release internal list items
  3717. destructor Destroy; override;
  3718. /// add a TSQLPropInfo to the list
  3719. function Add(aItem: TSQLPropInfo): integer;
  3720. /// find an item in the list
  3721. // - returns nil if not found
  3722. function ByRawUTF8Name(const aName: RawUTF8): TSQLPropInfo; overload;
  3723. {$ifdef HASINLINE}inline;{$endif}
  3724. /// find an item in the list
  3725. // - returns nil if not found
  3726. function ByName(aName: PUTF8Char): TSQLPropInfo; overload;
  3727. {$ifdef HASINLINE}inline;{$endif}
  3728. /// find an item in the list
  3729. // - returns -1 if not found
  3730. function IndexByName(const aName: RawUTF8): integer; overload;
  3731. {$ifdef HASINLINE}inline;{$endif}
  3732. /// find an item in the list
  3733. // - returns -1 if not found
  3734. function IndexByName(aName: PUTF8Char): integer; overload;
  3735. /// find an item by name in the list, including RowID/ID
  3736. // - will identify 'ID' / 'RowID' field name as -1
  3737. // - raise an EORMException if not found in the internal list
  3738. function IndexByNameOrExcept(const aName: RawUTF8): integer;
  3739. /// find one or several items by name in the list, including RowID/ID
  3740. // - will identify 'ID' / 'RowID' field name as -1
  3741. // - raise an EORMException if not found in the internal list
  3742. procedure IndexesByNamesOrExcept(const aNames: array of RawUTF8;
  3743. const aIndexes: array of PInteger);
  3744. /// find an item in the list, searching by unflattened name
  3745. // - for a flattened property, you may for instance call
  3746. // IndexByNameUnflattenedOrExcept('Address.Country.Iso')
  3747. // instead of IndexByNameOrExcept('Address_Country')
  3748. // - won't identify 'ID' / 'RowID' field names, just List[].
  3749. // - raise an EORMException if not found in the internal list
  3750. function IndexByNameUnflattenedOrExcept(const aName: RawUTF8): integer;
  3751. /// fill a TRawUTF8DynArray instance from the field names
  3752. // - excluding ID
  3753. procedure NamesToRawUTF8DynArray(var Names: TRawUTF8DynArray);
  3754. /// returns the number of TSQLPropInfo in the list
  3755. property Count: integer read fCount;
  3756. /// quick access to the TSQLPropInfo list
  3757. // - note that length(List) may not equal Count, since is its capacity
  3758. property List: TSQLPropInfoObjArray read fList;
  3759. /// read-only retrieval of a TSQLPropInfo item
  3760. // - will raise an exception if out of range
  3761. property Items[aIndex: integer]: TSQLPropInfo read GetItem; //default;
  3762. end;
  3763. /// simple writer to a Stream, specialized for writing an object as INI
  3764. // - resulting content will be UTF-8 encoded
  3765. // - use an internal buffer, faster than string+string
  3766. TINIWriter = class(TTextWriter)
  3767. /// write the published integer, Int64, floating point values, (wide)string,
  3768. // enumerates (e.g. boolean), variant properties of the object
  3769. // - won't handle shortstring properties
  3770. // - add a new INI-like section with [Value.ClassName] if WithSection is true
  3771. // - the object must have been compiled with the $M+ define, i.e. must
  3772. // inherit from TPersistent or TSQLRecord
  3773. // - the enumerates properties are stored with their integer index value
  3774. // - content can be read back using overloaded procedures ReadObject()
  3775. procedure WriteObject(Value: TObject; const SubCompName: RawUTF8='';
  3776. WithSection: boolean=true); reintroduce;
  3777. end;
  3778. /// method prototype to be used for custom serialization of a class
  3779. // - to be used with TJSONSerializer.RegisterCustomSerializer() method
  3780. // - note that the generated JSON content is not required to start with '{',
  3781. // as a normal JSON object (you may e.g. write a JSON string for some class) -
  3782. // as a consequence, custom code could explicitely start with Add('{')
  3783. // - implementation code shall follow function TJSONSerializer.WriteObject()
  3784. // patterns, i.e. aSerializer.Add/AddInstanceName/AddJSONEscapeString...
  3785. // - implementation code shall follow the same exact format for the
  3786. // associated TJSONSerializerCustomReader callback
  3787. TJSONSerializerCustomWriter = procedure(const aSerializer: TJSONSerializer;
  3788. aValue: TObject; aOptions: TTextWriterWriteObjectOptions) of object;
  3789. /// method prototype to be used for custom un-serialization of a class
  3790. // - to be used with TJSONSerializer.RegisterCustomSerializer() method
  3791. // - note that the read JSON content is not required to start with '{',
  3792. // as a normal JSON object (you may e.g. read a JSON string for some class) -
  3793. // as a consequence, custom code could explicitely start with "if aFrom^='{'..."
  3794. // - implementation code shall follow function JSONToObject() patterns, i.e.
  3795. // calling low-level GetJSONField() function to decode the JSON content
  3796. // - implementation code shall follow the same exact format for the
  3797. // associated TJSONSerializerCustomWriter callback
  3798. TJSONSerializerCustomReader = function(const aValue: TObject; aFrom: PUTF8Char;
  3799. var aValid: Boolean; aOptions: TJSONToObjectOptions): PUTF8Char of object;
  3800. /// several options to customize how TSQLRecord would be serialized
  3801. // - e.g. if properties storing JSON should be serialized as an object, and not
  3802. // escaped as a string (which is the default, matching ORM column storage)
  3803. // - if an additional "ID_str":"12345" field should be added to the standard
  3804. // "ID":12345 field, which may exceed 53-bit integer precision of JavsCript
  3805. TJSONSerializerSQLRecordOption = (
  3806. jwoAsJsonNotAsString, jwoID_str);
  3807. /// options to customize how TSQLRecord would be written by TJSONSerializer
  3808. TJSONSerializerSQLRecordOptions = set of TJSONSerializerSQLRecordOption;
  3809. /// simple writer to a Stream, specialized for writing an object as JSON
  3810. // - resulting JSON content will be UTF-8 encoded
  3811. // - use an internal buffer, faster than string+string
  3812. TJSONSerializer = class(TJSONWriter)
  3813. protected
  3814. fSQLRecordOptions: TJSONSerializerSQLRecordOptions;
  3815. procedure SetSQLRecordOptions(Value: TJSONSerializerSQLRecordOptions);
  3816. public
  3817. /// serialize as JSON the published integer, Int64, floating point values,
  3818. // TDateTime (stored as ISO 8601 text), string and enumerate (e.g. boolean)
  3819. // properties of the object
  3820. // - won't handle shortstring properties
  3821. // - the object must have been compiled with the $M+ define, i.e. must
  3822. // inherit from TPersistent or TSQLRecord, or has been defined with a
  3823. // custom serializer via RegisterCustomSerializer()
  3824. // - will write also the properties published in the parent classes
  3825. // - the enumerates properties are stored with their integer index value by
  3826. // default, but will be written as text if woFullExpand option is set
  3827. // - TList objects are not handled by default - they will be written only
  3828. // if FullExpand is set to true (and JSONToObject won't be able to read it)
  3829. // - nested properties are serialized as nested JSON objects
  3830. // - any TCollection property will also be serialized as JSON array
  3831. // - any TStrings or TRawUTF8List property will also be serialized as
  3832. // JSON string array
  3833. // - function ObjectToJSON() is just a wrapper over this method
  3834. procedure WriteObject(Value: TObject;
  3835. Options: TTextWriterWriteObjectOptions=[woDontStoreDefault]); override;
  3836. /// override method, handling IncludeUnitName option
  3837. procedure AddInstancePointer(Instance: TObject; SepChar: AnsiChar;
  3838. IncludeUnitName: boolean); override;
  3839. /// customize TSQLRecord.GetJSONValues serialization process
  3840. // - jwoAsJsonNotAsString would force TSQLRecord.GetJSONValues to serialize
  3841. // nested property instances as a JSON object/array, not a JSON string:
  3842. // i.e. root/table/id REST would be ready-to-be-consummed from AJAX clients
  3843. // (e.g. TSQLPropInfoRTTIObject.GetJSONValues as a JSON object, and
  3844. // TSQLPropInfoRTTIDynArray.GetJSONValues as a JSON array)
  3845. // - jwoID_str would add an "ID_str":"12345" property to the default
  3846. // "ID":12345 field to circumvent JavaScript's limitation of 53-bit for
  3847. // integer numbers, which is easily reached with our 64-bit TID values, e.g.
  3848. // if TSynUniqueIdentifier are used to generate the IDs: AJAX clients should
  3849. // better use this "ID_str" string value to identify each record, and ignore
  3850. // the "id" fields
  3851. property SQLRecordOptions: TJSONSerializerSQLRecordOptions
  3852. read fSQLRecordOptions write SetSQLRecordOptions;
  3853. /// define a custom serialization for a given class
  3854. // - by default, TSQLRecord, TPersistent, TStrings, TCollection classes
  3855. // are processed: but you can specify here some callbacks to perform
  3856. // the serialization process for any class
  3857. // - any previous registration is overridden
  3858. // - setting both aReader=aWriter=nil will return back to the default class
  3859. // serialization (i.e. published properties serialization)
  3860. // - note that any inherited classes will be serialized as the parent class
  3861. class procedure RegisterCustomSerializer(aClass: TClass;
  3862. aReader: TJSONSerializerCustomReader; aWriter: TJSONSerializerCustomWriter);
  3863. /// let a given class be recognized by JSONToObject() from "ClassName":".."
  3864. // - TObjectList item instances will be created corresponding to the
  3865. // serialized class name field specified, and JSONToNewObject() can create a
  3866. // new instance using the "ClassName":"..." field to identify the class type
  3867. // - by default, all referenced TSQLRecord classes will be globally
  3868. // registered when TSQLRecordProperties information is retrieved
  3869. class procedure RegisterClassForJSON(aItemClass: TClass); overload;
  3870. /// let a given class be recognized by JSONToObject() from "ClassName":".."
  3871. // - TObjectList item instances will be created corresponding to the
  3872. // serialized class name field specified, and JSONToNewObject() can create a
  3873. // new instance using the "ClassName":"..." field to identify the class type
  3874. // - by default, all referenced TSQLRecord classes will be globally
  3875. // registered when TSQLRecordProperties information is retrieved
  3876. class procedure RegisterClassForJSON(const aItemClass: array of TClass); overload;
  3877. {$ifndef LVCL}
  3878. /// let a given TCollection be recognized during JSON serialization
  3879. // - due to how TCollection instances are created, you can not create a
  3880. // server-side instance of TCollection directly
  3881. // - first workaround is to inherit from TInterfacedCollection
  3882. // - this method allows to recognize the needed TCollectionItem class for
  3883. // a given TCollection class, so allow to (un)serialize any TCollection,
  3884. // without defining a new method and inherits from TInterfacedCollection
  3885. // - note that both supplied classes will be registered for the internal
  3886. // "ClassName":"..." RegisterClassForJSON() process
  3887. class procedure RegisterCollectionForJSON(aCollection: TCollectionClass;
  3888. aItem: TCollectionItemClass);
  3889. {$endif}
  3890. /// let a T*ObjArray dynamic array be used for storage of class instances
  3891. // - will allow JSON serialization and unserialization of the registered
  3892. // dynamic array property defined in any TPersistent or TSQLRecord
  3893. // - could be used as such (note the T*ObjArray type naming convention):
  3894. // ! TUserObjArray = array of TUser;
  3895. // ! ...
  3896. // ! TJSONSerializer.RegisterObjArrayForJSON(TypeInfo(TUserObjArray),TUser);
  3897. // - then you can use ObjArrayAdd/ObjArrayFind/ObjArrayDelete to manage
  3898. // the stored items, and never forget to call ObjArrayClear to release
  3899. // the memory
  3900. class procedure RegisterObjArrayForJSON(aDynArray: PTypeInfo; aItem: TClass); overload;
  3901. /// let T*ObjArray dynamic arrays be used for storage of class instances
  3902. // - will allow JSON serialization and unserialization of the registered
  3903. // dynamic array property defined in any TPersistent or TSQLRecord
  3904. // - will call the overloaded RegisterObjArrayForJSON() class method by pair:
  3905. // ! TJSONSerializer.RegisterObjArrayForJSON([
  3906. // ! TypeInfo(TAddressObjArray),TAddress, TypeInfo(TUserObjArray),TUser]);
  3907. class procedure RegisterObjArrayForJSON(const aDynArrayClassPairs: array of const); overload;
  3908. /// retrieve TClassInstance information for a T*ObjArray dynamic array type
  3909. // - the T*ObjArray dynamic array should have been previously registered
  3910. // via TJSONSerializer.RegisterObjArrayForJSON() overloaded methods
  3911. // - returns nil if the supplied type is not a registered T*ObjArray
  3912. class function RegisterObjArrayFindType(aDynArray: PTypeInfo): PClassInstance;
  3913. end;
  3914. const
  3915. /// fake TTypeInfo RTTI used for TGUID on older versions of the compiler
  3916. GUID_FAKETYPEINFO: packed record
  3917. Kind: TTypeKind;
  3918. Name: string[5];
  3919. Size: cardinal;
  3920. Count: integer;
  3921. end = (
  3922. Kind: tkRecord;
  3923. Name: 'TGUID';
  3924. Size: sizeof(TGUID);
  3925. Count: 0);
  3926. /// retrieve a Field property RTTI information from a Property Name
  3927. function ClassFieldProp(ClassType: TClass; const PropName: shortstring): PPropInfo;
  3928. /// retrieve a Field property RTTI information from a Property Name
  3929. // - this special version also search into parent properties (default is only current)
  3930. function ClassFieldPropWithParents(aClassType: TClass; const PropName: shortstring): PPropInfo;
  3931. /// retrieve a class Field property instance from a Property Name
  3932. // - this version also search into parent properties
  3933. // - returns TRUE and set PropInstance if a matching property was found
  3934. function ClassFieldInstance(Instance: TObject; const PropName: shortstring;
  3935. PropClassType: TClass; out PropInstance): boolean; overload;
  3936. /// retrieve a Field property RTTI information from a Property Name
  3937. // - this special version also search into parent properties (default is only current)
  3938. function ClassFieldPropWithParentsFromUTF8(aClassType: TClass; PropName: PUTF8Char;
  3939. PropNameLen: integer): PPropInfo;
  3940. /// retrieve a Field property RTTI information searching for a Property class type
  3941. // - this special version also search into parent properties
  3942. function ClassFieldPropWithParentsFromClassType(aClassType,aSearchedClassType: TClass): PPropInfo;
  3943. /// retrieve a class Field property instance from a Property class type
  3944. // - this version also search into parent properties
  3945. // - returns TRUE and set PropInstance if a matching property was found
  3946. function ClassFieldInstance(Instance: TObject; PropClassType: TClass;
  3947. out PropInstance): boolean; overload;
  3948. /// retrieve a class instance property value matching a class type
  3949. // - if aSearchedInstance is aSearchedClassType, will return aSearchedInstance
  3950. // - if aSearchedInstance is not aSearchedClassType, it will try all nested
  3951. // properties of aSearchedInstance for a matching aSearchedClassType: if no
  3952. // exact match is found, will return aSearchedInstance
  3953. function ClassFieldPropInstanceMatchingClass(aSearchedInstance: TObject;
  3954. aSearchedClassType: TClass): TObject;
  3955. /// retrieve the total number of properties for a class, including its parents
  3956. function ClassFieldCountWithParents(ClassType: TClass;
  3957. onlyWithoutGetter: boolean=false): integer;
  3958. /// returns TRUE if the class has some published fields, including its parents
  3959. function ClassHasPublishedFields(ClassType: TClass): boolean;
  3960. /// retrieve all class hierachy types which have some published properties
  3961. function ClassHierarchyWithField(ClassType: TClass): TClassDynArray;
  3962. /// retrieve the PPropInfo values of all published properties of a class
  3963. // - you could select which property types should be included in the list
  3964. function ClassFieldAllProps(ClassType: TClass;
  3965. Types: TTypeKinds=[low(TTypeKind)..high(TTypeKind)]): PPropInfoDynArray;
  3966. /// retrieve the field names of all published properties of a class
  3967. // - will optionally append the property type to the name, e.g 'Age: integer'
  3968. // - you could select which property types should be included in the list
  3969. function ClassFieldNamesAllProps(ClassType: TClass; IncludePropType: boolean=false;
  3970. Types: TTypeKinds=[low(TTypeKind)..high(TTypeKind)]): TRawUTF8DynArray;
  3971. /// retrieve the field names of all published properties of a class
  3972. // - will optionally append the property type to the name, e.g 'Age: integer'
  3973. // - you could select which property types should be included in the list
  3974. function ClassFieldNamesAllPropsAsText(ClassType: TClass; IncludePropType: boolean=false;
  3975. Types: TTypeKinds=[low(TTypeKind)..high(TTypeKind)]): RawUTF8;
  3976. /// retrieve an object's component from its property name and class
  3977. // - useful to set User Interface component, e.g.
  3978. function GetObjectComponent(Obj: TPersistent; const ComponentName: shortstring;
  3979. ComponentClass: TClass): pointer;
  3980. /// retrieve the class property RTTI information for a specific class
  3981. function InternalClassProp(ClassType: TClass): PClassProp;
  3982. {$ifdef FPC}inline;{$endif}
  3983. /// retrieve the class property RTTI information for a specific class
  3984. // - will return the number of published properties
  3985. // - and set the PropInfo variable to point to the first property
  3986. // - typical use to enumerate all published properties could be:
  3987. // ! var i: integer;
  3988. // ! CT: TClass;
  3989. // ! P: PPropInfo;
  3990. // ! begin
  3991. // ! CT := ..;
  3992. // ! repeat
  3993. // ! for i := 1 to InternalClassPropInfo(CT,P) do begin
  3994. // ! // use P^
  3995. // ! P := P^.Next;
  3996. // ! end;
  3997. // ! CT := CT.ClassParent;
  3998. // ! until CT=nil;
  3999. // ! end;
  4000. // such a loop is much faster than using the RTL's TypeInfo or RTTI units
  4001. function InternalClassPropInfo(ClassType: TClass; out PropInfo: PPropInfo): integer;
  4002. /// retrieve a method RTTI information for a specific class
  4003. function InternalMethodInfo(aClassType: TClass; const aMethodName: ShortString): PMethodInfo;
  4004. /// execute an instance method from its RTTI per-interface information
  4005. // - calling this function with a pre-computed PInterfaceEntry value is faster
  4006. // than calling the TObject.GetInterface() method, especially when the class
  4007. // implements several interfaces, since it avoid a slow GUID lookup
  4008. function GetInterfaceFromEntry(Instance: TObject; Entry: PInterfaceEntry; out Obj): boolean;
  4009. /// retrieve the ready to be displayed text of an enumeration
  4010. // - will "uncamel" then translate into a generic VCL string
  4011. // - aIndex will be converted to the matching ordinal value (byte or word)
  4012. function GetEnumCaption(aTypeInfo: PTypeInfo; const aIndex): string;
  4013. /// get the corresponding enumeration name, without the first lowercase chars
  4014. // (otDone -> 'Done')
  4015. // - aIndex will be converted to the matching ordinal value (byte or word)
  4016. // - this will return the code-based English text; use GetEnumCaption() to
  4017. // retrieve the enumeration display text
  4018. function GetEnumNameTrimed(aTypeInfo: PTypeInfo; const aIndex): RawUTF8;
  4019. /// get all included values of an enumeration set, as CSV names
  4020. function GetSetNameCSV(aTypeInfo: PTypeInfo; const aValue): RawUTF8;
  4021. var
  4022. /// a shared list of T*ObjArray registered serializers
  4023. // - you should not access this variable, but via inline methods
  4024. ObjArraySerializers: TPointerClassHash;
  4025. /// fill a class instance from a TDocVariant object document properties
  4026. // - returns FALSE if the variant is not a dvObject, TRUE otherwise
  4027. function DocVariantToObject(var doc: TDocVariantData; obj: TObject): boolean;
  4028. /// fill a T*ObjArray variable from a TDocVariant array document values
  4029. // - will always erase the T*ObjArray instance, and fill it from arr values
  4030. procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray;
  4031. objClass: TClass); overload;
  4032. /// fill a T*ObjArray variable from a TDocVariant array document values
  4033. // - will always erase the T*ObjArray instance, and fill it from arr values
  4034. procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray;
  4035. objClass: PClassInstance); overload;
  4036. { ************ cross-cutting classes and types }
  4037. type
  4038. {$ifndef LVCL}
  4039. /// any TCollection used between client and server shall inherit from this class
  4040. // - you should override the GetClass virtual method to provide the
  4041. // expected collection item class to be used on server side
  4042. // - another possibility is to register a TCollection/TCollectionItem pair
  4043. // via a call to TJSONSerializer.RegisterCollectionForJSON()
  4044. TInterfacedCollection = class(TCollection)
  4045. protected
  4046. /// you shall override this abstract method
  4047. class function GetClass: TCollectionItemClass; virtual; abstract;
  4048. public
  4049. /// this constructor which will call GetClass to initialize the collection
  4050. constructor Create; reintroduce; virtual;
  4051. end;
  4052. /// class-reference type (metaclass) of a TInterfacedCollection kind
  4053. TInterfacedCollectionClass = class of TInterfacedCollection;
  4054. /// abstract TCollectionItem class, which will instantiate all its nested
  4055. // TPersistent/TSynPersistent class published properties, then release them when freed
  4056. // - could be used for gathering of TCollectionItem properties, e.g. for
  4057. // Domain objects in DDD, especially for list of value objects
  4058. // - note that non published properties won't be instantiated
  4059. // - please take care that you would not create any endless recursion: you
  4060. // should ensure that at one level, nested published properties won't have any
  4061. // class instance matching its parent type
  4062. // - since the destructor will release all nested properties, you should
  4063. // never store a reference of any of those nested instances outside
  4064. TCollectionItemAutoCreateFields = class(TCollectionItem)
  4065. public
  4066. /// this overriden constructor will instantiate all its nested
  4067. // TPersistent class published properties
  4068. constructor Create(Collection: TCollection); override;
  4069. /// finalize the instance, and release its published properties
  4070. destructor Destroy; override;
  4071. end;
  4072. {$endif LVCL}
  4073. /// abstract TPersistent class, which will instantiate all its nested TPersistent
  4074. // class published properties, then release them (and any T*ObjArray) when freed
  4075. // - TSynAutoCreateFields is to be preferred in most cases, due to its lower overhead
  4076. // - note that non published (e.g. public) properties won't be instantiated
  4077. // - please take care that you would not create any endless recursion: you
  4078. // should ensure that at one level, nested published properties won't have any
  4079. // class instance matching its parent type
  4080. // - since the destructor will release all nested properties, you should
  4081. // never store a reference of any of those nested instances outside
  4082. TPersistentAutoCreateFields = class(TPersistentWithCustomCreate)
  4083. public
  4084. /// this overriden constructor will instantiate all its nested
  4085. // TPersistent class published properties
  4086. constructor Create; override;
  4087. /// finalize the instance, and release its published properties
  4088. destructor Destroy; override;
  4089. end;
  4090. /// our own empowered TPersistentAutoCreateFields-like parent class
  4091. // - TPersistent/TPersistentAutoCreateFields have an unexpected speed overhead
  4092. // due a giant lock introduced to manage property name fixup resolution
  4093. // (which we won't use outside the VCL)
  4094. // - abstract class able with a virtual constructor, RTTI for published
  4095. // properties, and automatic memory management of all nested class
  4096. // published properties
  4097. // - will also release any T*ObjArray dynamic array storage of persistents,
  4098. // previously registered via TJSONSerializer.RegisterObjArrayForJSON()
  4099. // - this class is a perfect parent for any class storing data by value, e.g.
  4100. // DDD Value Objects, Entities or Aggregates
  4101. // - note that non published (e.g. public) properties won't be instantiated
  4102. // - please take care that you would not create any endless recursion: you
  4103. // should ensure that at one level, nested published properties won't have any
  4104. // class instance matching its parent type
  4105. // - since the destructor will release all nested properties, you should
  4106. // never store a reference to any of those nested instances if this owner
  4107. // may be freed before
  4108. TSynAutoCreateFields = class(TSynPersistent)
  4109. public
  4110. /// this overriden constructor will instantiate all its nested
  4111. // TPersistent/TSynPersistent/TSynAutoCreateFields class published properties
  4112. {$ifdef FPC_OR_PUREPASCAL}
  4113. constructor Create; override;
  4114. {$else}
  4115. class function NewInstance: TObject; override;
  4116. {$endif}
  4117. /// finalize the instance, and release its published properties
  4118. destructor Destroy; override;
  4119. end;
  4120. /// adding locking methods to a TSynAutoCreateFields with virtual constructor
  4121. TSynAutoCreateFieldsLocked = class(TSynAutoCreateFields)
  4122. protected
  4123. fSafe: TSynLocker;
  4124. public
  4125. /// initialize the object instance, and its associated lock
  4126. constructor Create; override;
  4127. /// release the instance (including the locking resource)
  4128. destructor Destroy; override;
  4129. /// access to the locking methods of this instance
  4130. // - use Safe.Lock/TryLock with a try ... finally Safe.Unlock block
  4131. property Safe: TSynLocker read fSafe;
  4132. /// could be used as a short-cut to Safe.Lock
  4133. procedure Lock; {$ifdef HASINLINE}inline;{$endif}
  4134. /// could be used as a short-cut to Safe.UnLock
  4135. procedure Unlock; {$ifdef HASINLINE}inline;{$endif}
  4136. end;
  4137. /// abstract TInterfacedObject class, which will instantiate all its nested
  4138. // TPersistent/TSynPersistent published properties, then release them when freed
  4139. // - could be used for gathering of TCollectionItem properties, e.g. for
  4140. // Domain objects in DDD, especially for list of value objects
  4141. // - note that non published properties won't be instantiated
  4142. // - please take care that you would not create any endless recursion: you
  4143. // should ensure that at one level, nested published properties won't have any
  4144. // class instance matching its parent type
  4145. // - since the destructor will release all nested properties, you should
  4146. // never store a reference of any of those nested instances outside
  4147. TInterfacedObjectAutoCreateFields = class(TInterfacedObjectWithCustomCreate)
  4148. public
  4149. /// this overriden constructor will instantiate all its nested
  4150. // TPersistent/TSynPersistent/TSynAutoCreateFields class published properties
  4151. constructor Create; override;
  4152. /// finalize the instance, and release its published properties
  4153. destructor Destroy; override;
  4154. end;
  4155. /// used by TRawUTF8ObjectCacheList to manage a list of information cache
  4156. TRawUTF8ObjectCacheSettings = class(TSynPersistent)
  4157. protected
  4158. fTimeOutMS: integer;
  4159. fPurgePeriodMS: integer;
  4160. public
  4161. /// will set default values to settings
  4162. constructor Create; override;
  4163. published
  4164. /// period after which the cache information should be flushed
  4165. // - use -1 to disable time out; any big value would be limited to 10 minutes
  4166. // - default is 120000, i.e. 2 minutes
  4167. property TimeOutMS: integer read fTimeOutMS write fTimeOutMS;
  4168. // period after which TRawUTF8ObjectCacheList would search for expired entries
  4169. // - use -1 to disable purge (not adviced, since may break process)
  4170. // - default is 1000, i.e. 1 second
  4171. property PurgePeriodMS: integer read fPurgePeriodMS write fPurgePeriodMS;
  4172. end;
  4173. TRawUTF8ObjectCacheList = class;
  4174. /// maintain information cache for a given key
  4175. // - after a given period of time, the entry is not deleted, but CacheClear
  4176. // virtual method is called to release the associated data or services
  4177. // - inherit from this abstract class to store your own key-defined information
  4178. // or you own interface-based services
  4179. TRawUTF8ObjectCache = class(TSynAutoCreateFieldsLocked)
  4180. protected
  4181. fKey: RawUTF8; // inherited class could publish fKey with a custom name
  4182. fOwner: TRawUTF8ObjectCacheList;
  4183. fTimeoutMS: integer;
  4184. fTimeoutTix: Int64;
  4185. /// should be called by inherited classes when information or services are set
  4186. // - set fTimeoutTix according to fTimeoutMS, to enable timeout mechanism
  4187. // - could be used when the content is refreshed, to increase the entry TTL
  4188. // - caller should do Safe.Lock to ensure thread-safety
  4189. procedure CacheSet; virtual;
  4190. /// called by Destroy and TRawUTF8ObjectCacheList.DoPurge
  4191. // - set fTimeoutTix := 0 (inherited should also release services interfaces)
  4192. // - protected by Safe.Lock from TRawUTF8ObjectCacheList.DoPurge
  4193. procedure CacheClear; virtual;
  4194. public
  4195. /// initialize the information cache entry
  4196. // - should not be called directly, but by TRawUTF8ObjectCacheList.GetLocked
  4197. constructor Create(aOwner: TRawUTF8ObjectCacheList; const aKey: RawUTF8); reintroduce; virtual;
  4198. /// finalize the information cache entry
  4199. // - would also call the virtual CacheClear method
  4200. destructor Destroy; override;
  4201. /// Dependency Injection using fOwner.OnKeyResolve, for the current Key
  4202. function Resolve(const aInterface: TGUID; out Obj): boolean;
  4203. /// access to the associated storage list
  4204. property Owner: TRawUTF8ObjectCacheList read fOwner;
  4205. end;
  4206. /// class-reference type (metaclass) of a TRawUTF8ObjectCache
  4207. // - used e.g. by TRawUTF8ObjectCacheClass.Create to generate the
  4208. // expected cache instances
  4209. TRawUTF8ObjectCacheClass = class of TRawUTF8ObjectCache;
  4210. /// manage a list of information cache, identified by a hashed key
  4211. // - you should better inherit from this class, to give a custom name and
  4212. // constructor, or alter the default behavior
  4213. // - would maintain a list of TRawUTF8ObjectCache instances
  4214. TRawUTF8ObjectCacheList = class(TRawUTF8ListHashedLocked)
  4215. protected
  4216. fSettings: TRawUTF8ObjectCacheSettings;
  4217. fLog: TSynLogFamily;
  4218. fLogEvent: TSynLogInfo;
  4219. fClass: TRawUTF8ObjectCacheClass;
  4220. fNextPurgeTix: Int64;
  4221. fOnKeyResolve: TOnKeyResolve;
  4222. procedure DoPurge; virtual;
  4223. // returns fClass.Create by default: inherited classes may add custom check
  4224. // or return nil if Key is invalid
  4225. function NewObjectCache(const Key: RawUTF8): TRawUTF8ObjectCache; virtual;
  4226. public
  4227. /// initialize the cache-information for a given class
  4228. // - inherited classes may reintroduce a new constructor, for ease of use
  4229. constructor Create(aClass: TRawUTF8ObjectCacheClass;
  4230. aSettings: TRawUTF8ObjectCacheSettings; aLog: TSynLogFamily; aLogEvent: TSynLogInfo;
  4231. const aOnKeyResolve: TOnKeyResolve); reintroduce;
  4232. /// fill TRawUTF8ObjectCache with the matching key information
  4233. // - an unknown key, but with a successful NewObjectCache() call, will
  4234. // create and append a new fClass instance to the list (if onlyexisting
  4235. // is left to its default FALSE)
  4236. // - global or key-specific purge would be performed, if needed
  4237. // - on success (true), output cache instance would be locked
  4238. function GetLocked(const Key: RawUTF8; out cache: TRawUTF8ObjectCache;
  4239. onlyexisting: boolean=false): boolean; virtual;
  4240. /// you may call this method regularly to check for a needed purge
  4241. // - if Settings.PurgePeriodMS is reached, each TRawUTF8ObjectCache instance
  4242. // would check for its TimeOutMS and call CacheClear if information is outdated
  4243. procedure TryPurge;
  4244. /// this method will clear all associated information
  4245. // - a regular Clear would destroy all TRawUTF8ObjectCache instances,
  4246. // whereas this method would call CacheClear on each entry, so would
  4247. // be more thread-safe and efficient in pratice
  4248. procedure ForceCacheClear;
  4249. /// access to the associated logging instance
  4250. procedure Log(const TextFmt: RawUTF8; const TextArgs: array of const;
  4251. Level: TSynLogInfo = sllNone);
  4252. /// optional service locator for by-key Dependency Injection
  4253. property OnKeyResolve: TOnKeyResolve read fOnKeyResolve write fOnKeyResolve;
  4254. end;
  4255. const
  4256. /// HTML Status Code for "Continue"
  4257. HTML_CONTINUE = 100;
  4258. /// HTML Status Code for "Switching Protocols"
  4259. HTML_SWITCHINGPROTOCOLS = 101;
  4260. /// HTML Status Code for "Success"
  4261. HTML_SUCCESS = 200;
  4262. /// HTML Status Code for "Created"
  4263. HTML_CREATED = 201;
  4264. /// HTML Status Code for "Accepted"
  4265. HTML_ACCEPTED = 202;
  4266. /// HTML Status Code for "Non-Authoritative Information"
  4267. HTML_NONAUTHORIZEDINFO = 203;
  4268. /// HTML Status Code for "No Content"
  4269. HTML_NOCONTENT = 204;
  4270. /// HTML Status Code for "Multiple Choices"
  4271. HTML_MULTIPLECHOICES = 300;
  4272. /// HTML Status Code for "Moved Permanently"
  4273. HTML_MOVEDPERMANENTLY = 301;
  4274. /// HTML Status Code for "Found"
  4275. HTML_FOUND = 302;
  4276. /// HTML Status Code for "See Other"
  4277. HTML_SEEOTHER = 303;
  4278. /// HTML Status Code for "Not Modified"
  4279. HTML_NOTMODIFIED = 304;
  4280. /// HTML Status Code for "Use Proxy"
  4281. HTML_USEPROXY = 305;
  4282. /// HTML Status Code for "Temporary Redirect"
  4283. HTML_TEMPORARYREDIRECT = 307;
  4284. /// HTML Status Code for "Bad Request"
  4285. HTML_BADREQUEST = 400;
  4286. /// HTML Status Code for "Unauthorized"
  4287. HTML_UNAUTHORIZED = 401;
  4288. /// HTML Status Code for "Forbidden"
  4289. HTML_FORBIDDEN = 403;
  4290. /// HTML Status Code for "Not Found"
  4291. HTML_NOTFOUND = 404;
  4292. // HTML Status Code for "Method Not Allowed"
  4293. HTML_NOTALLOWED = 405;
  4294. // HTML Status Code for "Not Acceptable"
  4295. HTML_NOTACCEPTABLE = 406;
  4296. // HTML Status Code for "Proxy Authentication Required"
  4297. HTML_PROXYAUTHREQUIRED = 407;
  4298. /// HTML Status Code for "Request Time-out"
  4299. HTML_TIMEOUT = 408;
  4300. /// HTML Status Code for "Internal Server Error"
  4301. HTML_SERVERERROR = 500;
  4302. /// HTML Status Code for "Not Implemented"
  4303. HTML_NOTIMPLEMENTED = 501;
  4304. /// HTML Status Code for "Bad Gateway"
  4305. HTML_BADGATEWAY = 502;
  4306. /// HTML Status Code for "Service Unavailable"
  4307. HTML_UNAVAILABLE = 503;
  4308. /// HTML Status Code for "Gateway Timeout"
  4309. HTML_GATEWAYTIMEOUT = 504;
  4310. /// HTML Status Code for "HTTP Version Not Supported"
  4311. HTML_HTTPVERSIONNONSUPPORTED = 505;
  4312. /// you can use this cookie value to delete a cookie on the browser side
  4313. COOKIE_EXPIRED = '; Expires=Sat, 01 Jan 2010 00:00:01 GMT';
  4314. /// used e.g. by THttpApiServer.Request for http.sys to send a static file
  4315. // - the OutCustomHeader should contain the proper 'Content-type: ....'
  4316. // corresponding to the file (e.g. by calling GetMimeContentType() function
  4317. // from SynCommons supplyings the file name)
  4318. // - should match HTTP_RESP_STATICFILE constant defined in SynCrtSock.pas unit
  4319. STATICFILE_CONTENT_TYPE = '!STATICFILE';
  4320. /// used to notify e.g. the THttpServerRequest not to wait for any response
  4321. // from the client
  4322. // - is not to be used in normal HTTP process, but may be used e.g. by
  4323. // TWebSocketProtocolRest.ProcessFrame() to avoid to wait for an incoming
  4324. // response from the other endpoint
  4325. // - should match HTTP_RESP_NORESPONSE constant defined in SynCrtSock.pas unit
  4326. NORESPONSE_CONTENT_TYPE = '!NORESPONSE';
  4327. /// HTTP header used e.g. by THttpApiServer.Request for http.sys to send
  4328. // a static file in kernel mode
  4329. STATICFILE_CONTENT_TYPE_HEADER = HEADER_CONTENT_TYPE+STATICFILE_CONTENT_TYPE;
  4330. /// uppercase version of HTTP header for static file content serving
  4331. STATICFILE_CONTENT_TYPE_HEADER_UPPPER = HEADER_CONTENT_TYPE_UPPER+STATICFILE_CONTENT_TYPE;
  4332. /// convert any HTML_* constant to a short English text
  4333. // - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  4334. procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8); overload;
  4335. /// convert any HTML_* constant to an integer error code and its English text
  4336. // - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  4337. function StatusCodeToErrorMsg(Code: integer): RawUTF8; overload;
  4338. /// returns true for SUCCESS (200), CREATED (201), NOCONTENT (204),
  4339. // NOTMODIFIED (304) or TEMPORARYREDIRECT (307) codes
  4340. function StatusCodeIsSuccess(Code: integer): boolean;
  4341. {$ifdef HASINLINE}inline;{$endif}
  4342. type
  4343. /// the available HTTP methods transmitted between client and server
  4344. // - some custom verbs are available in addition to standard REST commands
  4345. // - most of iana verbs are available
  4346. // see http://www.iana.org/assignments/http-methods/http-methods.xhtml
  4347. // - for basic CRUD operations, we considered Create=mPOST, Read=mGET,
  4348. // Update=mPUT and Delete=mDELETE
  4349. TSQLURIMethod = (mNone, mGET, mPOST, mPUT, mDELETE, mHEAD,
  4350. mBEGIN, mEND, mABORT, mLOCK, mUNLOCK, mSTATE,
  4351. mOPTIONS, mPROPFIND, mPROPPATCH, mTRACE, mCOPY,
  4352. mMKCOL, mMOVE, mPURGE, mREPORT, mMKACTIVITY,
  4353. mMKCALENDAR,mCHECKOUT, mMERGE, mNOTIFY, mPATCH,
  4354. mSEARCH, mCONNECT);
  4355. /// set of available HTTP methods transmitted between client and server
  4356. TSQLURIMethods = set of TSQLURIMethod;
  4357. /// convert a string HTTP verb into its TSQLURIMethod enumerate
  4358. function StringToMethod(const method: RawUTF8): TSQLURIMethod;
  4359. {$ifdef MSWINDOWS}
  4360. {$ifdef ISDELPHIXE} // fix Delphi XE imcompatilibility
  4361. type
  4362. TSecurityAttributes = packed record
  4363. nLength: DWORD;
  4364. lpSecurityDescriptor: Pointer;
  4365. bInheritHandle: BOOL;
  4366. end;
  4367. const
  4368. SECURITY_DESCRIPTOR_REVISION = 1;
  4369. SECURITY_DESCRIPTOR_MIN_LENGTH = 20;
  4370. {$endif ISDELPHIXE}
  4371. {$endif MSWINDOWS}
  4372. { ******************* process monitoring / statistics }
  4373. type
  4374. /// the time periods covered by TSynMonitorUsage process
  4375. // - defines the resolution of information computed and stored
  4376. TSynMonitorUsageGranularity = (
  4377. mugUndefined,
  4378. mugMinute,
  4379. mugHour,
  4380. mugDay,
  4381. mugMonth,
  4382. mugYear);
  4383. /// defines one or several time periods for TSynMonitorUsage process
  4384. TSynMonitorUsageGranularities = set of TSynMonitorUsageGranularity;
  4385. /// how the TSynMonitorUsage storage IDs are computed
  4386. // - stored e.g. in TSQLMonitorUsage.ID primary key (after a shift)
  4387. // - it follows a 23 bit pattern of hour (5 bit), day (5 bit), month (4 bit),
  4388. // year (9 bit - starting at 2016) so that it is monotonic over time
  4389. // - by default, will store the information using mugHour granularity (i.e.
  4390. // values for the 60 minutes in a record), and pseudo-hours of 29, 30 and 31
  4391. // (see USAGE_ID_HOURMARKER[]) would identify mugDay, mugMonth and mugYear
  4392. // consolidated statistics
  4393. // - it would therefore store up to 24*365+365+12+1 = 9138 records per year
  4394. // in the associated storage engine (so there is no actual need to purge it)
  4395. TSynMonitorUsageID = object
  4396. public
  4397. /// the TID, as computed from time and granularity
  4398. Value: integer;
  4399. /// computes an ID corresponding to mugHour granularity of a given time
  4400. // - minutes and seconds would be ignored
  4401. // - mugHour granularity would store 0..59 information about each minute
  4402. procedure From(Y,M,D,H: integer); overload;
  4403. /// computes an ID corresponding to mugDay granularity of a given time
  4404. // - hours, minutes and seconds would be merged
  4405. // - mugDay granularity would store 0..23 information about each hour
  4406. // - a pseudo hour of 29 (i.e. USAGE_ID_HOURMARKER[mugDay]) is used
  4407. procedure From(Y,M,D: integer); overload;
  4408. /// computes an ID corresponding to mugMonth granularity of a given time
  4409. // - days, hours, minutes and seconds would be merged
  4410. // - mugMonth granularity would store 0..31 information about each day
  4411. // - a pseudo hour of 30 (i.e. USAGE_ID_HOURMARKER[mugMonth]) is used
  4412. procedure From(Y,M: integer); overload;
  4413. /// computes an ID corresponding to mugYear granularity of a given time
  4414. // - months, days, hours, minutes and seconds would be merged
  4415. // - mugYear granularity would store 0..11 information about each month
  4416. // - a pseudo hour of 31 (i.e. USAGE_ID_HOURMARKER[mugYear]) is used
  4417. procedure From(Y: integer); overload;
  4418. /// computes an ID corresponding to a given time
  4419. // - will set the ID with mugHour granularity, i.e. the information about
  4420. // the given hour, stored as per minute 0..59 values
  4421. // - minutes and seconds in supplied TimeLog value would therefore be ignored
  4422. procedure FromTimeLog(const TimeLog: TTimeLog);
  4423. /// computes an ID corresponding to the current UTC date/time
  4424. // - minutes and seconds would be ignored
  4425. procedure FromNowUTC;
  4426. /// returns the date/time
  4427. // - minutes and seconds would set to 0
  4428. function ToTimeLog: TTimeLog;
  4429. /// convert to Iso-8601 encoded text
  4430. function Text(Expanded: boolean; FirstTimeChar: AnsiChar = 'T'): RawUTF8;
  4431. /// retrieve the resolution of the stored information
  4432. // - i.e. either mugHour, mugDay, mugMonth or mugYear, which would store
  4433. // a true 0..23 hour value (for mugHour), or 29/30/31 pseudo-hour (i.e.
  4434. // USAGE_ID_HOURMARKER[mugDay/mugMonth/mugYear])
  4435. function Granularity: TSynMonitorUsageGranularity;
  4436. /// change the resolution of the stored information
  4437. procedure Truncate(gran: TSynMonitorUsageGranularity);
  4438. /// low-level read of a time field stored in this ID, per granularity
  4439. function GetTime(gran: TSynMonitorUsageGranularity): integer;
  4440. {$ifdef HASINLINE}inline;{$endif}
  4441. /// low-level modification of a time field stored in this ID, per granularity
  4442. procedure SetTime(gran: TSynMonitorUsageGranularity; aValue: integer);
  4443. end;
  4444. TSynMonitorUsageTrackProp = record
  4445. Info: PPropInfo;
  4446. /// property type, as recognized by MonitorPropUsageValue()
  4447. Kind: TSynMonitorType;
  4448. Name: RawUTF8;
  4449. Values: array[mugHour..mugYear] of TInt64DynArray;
  4450. CumulativeLast: Int64;
  4451. end;
  4452. TSynMonitorUsageTrackPropDynArray = array of TSynMonitorUsageTrackProp;
  4453. TSynMonitorUsageTrack = record
  4454. Instance: TObject;
  4455. Name: RawUTF8;
  4456. Props: TSynMonitorUsageTrackPropDynArray;
  4457. end;
  4458. PSynMonitorUsageTrackProp = ^TSynMonitorUsageTrackProp;
  4459. PSynMonitorUsageTrack = ^TSynMonitorUsageTrack;
  4460. /// abstract class to track, compute and store TSynMonitor detailed statistics
  4461. // - you should inherit from this class to implement proper data persistence,
  4462. // e.g. using TSynMonitorUsageRest for ORM-based storage
  4463. TSynMonitorUsage = class(TSynPersistentLocked)
  4464. protected
  4465. fLog: TSynLogFamily;
  4466. fTracked: array of TSynMonitorUsageTrack;
  4467. fValues: array[mugHour..mugYear] of Variant;
  4468. fCustomWritePropGranularity: TSynMonitorUsageGranularity;
  4469. fLastInstance: TObject;
  4470. fLastTrack: PSynMonitorUsageTrack;
  4471. fPrevious: TTimeLogBits;
  4472. fComment: RawUTF8;
  4473. function TrackPropLock(Instance: TObject; Info: PPropInfo): PSynMonitorUsageTrackProp;
  4474. // those methods would be protected (e.g. in Modified) by fSafe.Lock:
  4475. procedure SavePrevious(Scope: TSynMonitorUsageGranularity);
  4476. procedure Save(ID: TSynMonitorUsageID; Gran, Scope: TSynMonitorUsageGranularity);
  4477. function Load(const Time: TTimeLogBits): boolean;
  4478. procedure LoadTrack(var Track: TSynMonitorUsageTrack);
  4479. // should be overriden with proper persistence storage:
  4480. function SaveDB(ID: integer; const Track: variant; Gran: TSynMonitorUsageGranularity): boolean; virtual; abstract;
  4481. function LoadDB(ID: integer; Gran: TSynMonitorUsageGranularity; out Track: variant): boolean; virtual; abstract;
  4482. // may be overriden for testing purposes
  4483. procedure SetCurrentUTCTime(out minutes: TTimeLogBits); virtual;
  4484. public
  4485. /// finalize the statistics, saving any pending information
  4486. destructor Destroy; override;
  4487. /// track the values of one named object instance
  4488. // - would recognize the TSynMonitor* properties as TSynMonitorType from
  4489. // RTTI, using MonitorPropUsageValue(), within any (nested) object
  4490. // - the instance would be stored in fTracked[].Instance: ensure it would
  4491. // stay available during the whole TSynMonitorUsage process
  4492. function Track(Instance: TObject; const Name: RawUTF8=''): integer; overload; virtual;
  4493. /// track the values of the given object instances
  4494. // - would recognize the TSynMonitor* properties as TSynMonitorType from
  4495. // RTTI, using MonitorPropUsageValue(), within any (nested) object
  4496. // - instances would be stored in fTracked[].Instance: ensure they would
  4497. // stay available during the whole TSynMonitorUsage process
  4498. procedure Track(const Instances: array of TSynMonitor); overload;
  4499. /// to be called when tracked properties changed on a tracked class instance
  4500. procedure Modified(Instance: TObject); overload;
  4501. /// to be called when tracked properties changed on a tracked class instance
  4502. procedure Modified(Instance: TObject; const PropNames: array of RawUTF8); overload; virtual;
  4503. /// some custom text, associated with the current stored state
  4504. // - would be persistented by Save() methods
  4505. property Comment: RawUTF8 read fComment write fComment;
  4506. end;
  4507. const
  4508. USAGE_VALUE_LEN: array[mugHour..mugYear] of integer = (60,24,31,12);
  4509. USAGE_ID_SHIFT: array[mugHour..mugYear] of byte = (0,5,10,14);
  4510. USAGE_ID_BITS: array[mugHour..mugYear] of byte = (5,5,4,9);
  4511. USAGE_ID_MASK: array[mugHour..mugYear] of integer = (31,31,15,511);
  4512. USAGE_ID_MAX: array[mugHour..mugYear] of cardinal = (23,30,11,127);
  4513. USAGE_ID_HOURMARKER: array[mugDay..mugYear] of integer = (29,30,31);
  4514. USAGE_ID_YEAROFFSET = 2016;
  4515. /// kind of "cumulative" TSynMonitorType stored in TSynMonitor / TSynMonitorUsage
  4516. // - those properties would have their values reset for each granularity level
  4517. // - would recognize TSynMonitorTotalMicroSec, TSynMonitorTotalBytes,
  4518. // TSynMonitorOneBytes, TSynMonitorBytesPerSec, TSynMonitorCount and
  4519. // TSynMonitorCount64 types
  4520. SYNMONITORVALUE_CUMULATIVE = [smvMicroSec,smvBytes,smvCount, smvCount64];
  4521. function ToText(gran: TSynMonitorUsageGranularity): PShortString; overload;
  4522. /// guess the kind of value stored in a TSynMonitor / TSynMonitorUsage property
  4523. // - would recognize TSynMonitorTotalMicroSec, TSynMonitorOneMicroSec,
  4524. // TSynMonitorTotalBytes, TSynMonitorOneBytes, TSynMonitorBytesPerSec,
  4525. // TSynMonitorCount and TSynMonitorCount64 types from supplied RTTI
  4526. function MonitorPropUsageValue(info: PPropInfo): TSynMonitorType;
  4527. { ************ main ORM / SOA classes and types }
  4528. const
  4529. /// the used TAuthSession.IDCardinal value if the session not started yet
  4530. // - i.e. if the session handling is still in its handshaking phase
  4531. CONST_AUTHENTICATION_SESSION_NOT_STARTED = 0;
  4532. /// the used TAuthSession.IDCardinal value if authentication mode is not set
  4533. // - i.e. if TSQLRest.HandleAuthentication equals FALSE
  4534. CONST_AUTHENTICATION_NOT_USED = 1;
  4535. /// maximum handled dimension for TSQLRecordRTree
  4536. // - this value is the one used by SQLite3 R-Tree virtual table
  4537. RTREE_MAX_DIMENSION = 5;
  4538. /// used as "stored AS_UNIQUE" published property definition in TSQLRecord
  4539. AS_UNIQUE = false;
  4540. /// custom contract value to ignore contract validation from client side
  4541. // - you could set the aContractExpected parameter to this value for
  4542. // TSQLRestClientURI.ServiceDefine or TSQLRestClientURI.ServiceRegister
  4543. // so that the contract won't be checked with the server
  4544. // - it would be used e.g. if the remote server is not a mORMot server,
  4545. // but a plain REST/HTTP server - e.g. for public API notifications
  4546. SERVICE_CONTRACT_NONE_EXPECTED = '*';
  4547. type
  4548. TSQLTable = class;
  4549. {$M+} { we need the RTTI information to be compiled for the published
  4550. properties of these classes and their children (like TPersistent),
  4551. to enable ORM - must be defined at the forward definition level }
  4552. TSQLRecordProperties = class;
  4553. TSQLModel = class;
  4554. TSQLModelRecordProperties = class;
  4555. TSQLRecord = class; // published properties = ORM fields/columns
  4556. TSQLRecordMany = class;
  4557. TSQLAuthUser = class;
  4558. TSQLRest = class;
  4559. TSQLRestClient = class;
  4560. {.$METHODINFO ON} // this would include public methods as RESTful callbacks :(
  4561. TSQLRestServer = class;
  4562. {.$METHODINFO OFF}
  4563. TSQLRestStorage = class;
  4564. TSQLRestStorageRemote = class;
  4565. TSQLRestClientURI = class;
  4566. TInterfaceFactory = class;
  4567. TSQLRestBatch = class;
  4568. TSQLRestBatchLocked = class;
  4569. {$M-}
  4570. /// class-reference type (metaclass) of TSQLRecord
  4571. TSQLRecordClass = class of TSQLRecord;
  4572. PClass = ^TClass;
  4573. PSQLRecordClass = ^TSQLRecordClass;
  4574. /// a dynamic array storing TSQLRecord instances
  4575. // - not used direcly, but as specialized T*ObjArray types
  4576. TSQLRecordObjArray = array of TSQLRecord;
  4577. /// a dynamic array used to store the TSQLRecord classes in a Database Model
  4578. TSQLRecordClassDynArray = array of TSQLRecordClass;
  4579. /// exception raised in case of incorrect TSQLTable.Step / Field*() use
  4580. ESQLTableException = class(ESynException);
  4581. /// generic parent class of all custom Exception types of this unit
  4582. EORMException = class(ESynException);
  4583. /// exception raised in case of wrong Model definition
  4584. EModelException = class(EORMException);
  4585. /// exception raised in case of unexpected parsing error
  4586. EParsingException = class(EORMException);
  4587. /// exception raised in case of a Client-Server communication error
  4588. ECommunicationException = class(EORMException);
  4589. /// exception raised in case of an error in project implementation logic
  4590. EBusinessLayerException = class(EORMException);
  4591. /// exception raised in case of any authentication error
  4592. ESecurityException = class(EORMException);
  4593. /// exception dedicated to interface factory, e.g. services and mock/stubs
  4594. EInterfaceFactoryException = class(ESynException);
  4595. /// exception raised in case of Dependency Injection (aka IoC) issue
  4596. EInterfaceResolverException = class(ESynException);
  4597. /// exception dedicated to interface based service implementation
  4598. EServiceException = class(EORMException);
  4599. /// information about a TSQLRecord class property
  4600. // - sftID for TSQLRecord properties, which are pointer(RecordID), not
  4601. // any true class instance
  4602. // - sftMany for TSQLRecordMany properties, for which no data is
  4603. // stored in the table itself, but in a pivot table
  4604. // - sftObject for e.g. TStrings TRawUTF8List TCollection instances
  4605. {$ifdef CPU64}
  4606. TSQLPropInfoRTTIInstance = class(TSQLPropInfoRTTIInt64)
  4607. {$else}
  4608. TSQLPropInfoRTTIInstance = class(TSQLPropInfoRTTIInt32)
  4609. {$endif}
  4610. protected
  4611. fObjectClass: TClass;
  4612. public
  4613. /// will setup the corresponding ObjectClass property
  4614. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType); override;
  4615. /// direct access to the property class instance
  4616. function GetInstance(Instance: TObject): TObject;
  4617. {$ifdef HASINLINE}inline;{$endif}
  4618. /// direct access to the property class instance
  4619. procedure SetInstance(Instance, Value: TObject);
  4620. {$ifdef HASINLINE}inline;{$endif}
  4621. /// direct access to the property class
  4622. // - can be used e.g. for TSQLRecordMany properties
  4623. property ObjectClass: TClass read fObjectClass;
  4624. end;
  4625. /// information about a TRecordReference/TRecordReferenceToBeDeleted
  4626. // published property
  4627. // - identified as a sftRecord kind of property
  4628. TSQLPropInfoRTTIRecordReference = class(TSQLPropInfoRTTIInt64)
  4629. protected
  4630. fCascadeDelete: boolean;
  4631. public
  4632. /// will identify TRecordReferenceToBeDeleted kind of field, and
  4633. // setup the corresponding CascadeDelete property
  4634. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType); override;
  4635. /// TRUE if this sftRecord is a TRecordReferenceToBeDeleted
  4636. property CascadeDelete: boolean read fCascadeDelete;
  4637. end;
  4638. /// information about a TID published property
  4639. // - identified as a sftTID kind of property, optionally tied to a TSQLRecord
  4640. // class, via its custom type name, e.g.
  4641. // ! TSQLRecordClientID = type TID; -> TSQLRecordClient class
  4642. TSQLPropInfoRTTITID = class(TSQLPropInfoRTTIRecordReference)
  4643. protected
  4644. fRecordClass: TSQLRecordClass;
  4645. public
  4646. /// will setup the corresponding RecordClass property from the TID type name
  4647. // - the TSQLRecord type should have previously been registered to the
  4648. // TJSONSerializer.RegisterClassForJSON list, e.g. in TSQLModel.Create, so
  4649. // that e.g. 'TSQLRecordClientID' type name would match TSQLRecordClient
  4650. // - in addition, the '...ToBeDeletedID' name pattern would set CascadeDelete
  4651. constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType); override;
  4652. /// the TSQLRecord class associated to this TID
  4653. // - is computed from its type name - for instance, if you define:
  4654. // ! type
  4655. // ! TSQLRecordClientID = type TID;
  4656. // ! TSQLOrder = class(TSQLRecord)
  4657. // ! ...
  4658. // ! published OrderedBy: TSQLRecordClientID read fOrderedBy write fOrderedBy;
  4659. // ! ...
  4660. // then this OrderedBy property would be tied to the TSQLRecordClient class
  4661. // of the corresponding model, and the field value will be reset to 0 when
  4662. // the targetting record is deleted (emulating a ON DELETE SET DEFAULT)
  4663. property RecordClass: TSQLRecordClass read fRecordClass;
  4664. /// TRUE if this sftTID type name follows the '...ToBeDeletedID' pattern
  4665. // - e.g. 'TSQLRecordClientToBeDeletedID' type name would match
  4666. // TSQLRecordClient and set CascadeDelete
  4667. // - is computed from its type name - for instance, if you define:
  4668. // ! type
  4669. // ! TSQLRecordClientToBeDeletedID = type TID;
  4670. // ! TSQLOrder = class(TSQLRecord)
  4671. // ! ...
  4672. // ! published OrderedBy: TSQLRecordClientToBeDeletedID read fOrderedBy write fOrderedBy;
  4673. // ! ...
  4674. // then this OrderedBy property would be tied to the TSQLRecordClient class
  4675. // of the corresponding model, and the whole record will be deleted when
  4676. // the targetting record is deleted (emulating a ON DELETE CASCADE)
  4677. property CascadeDelete: boolean read fCascadeDelete;
  4678. end;
  4679. /// information about a TRecordVersion published property
  4680. // - identified as a sftRecordVersion kind of property, to track changes
  4681. TSQLPropInfoRTTIRecordVersion = class(TSQLPropInfoRTTIInt64);
  4682. /// information about a TSQLRecord class TSQLRecord property
  4683. // - kind sftID, which are pointer(RecordID), not any true class instance
  4684. // - will store the content just as an integer value
  4685. // - will recognize any instance pre-allocated via Create*Joined() constructor
  4686. TSQLPropInfoRTTIID = class(TSQLPropInfoRTTIInstance)
  4687. public
  4688. /// raise an exception if was created by Create*Joined() constructor
  4689. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  4690. /// this method will recognize if the TSQLRecord was allocated by
  4691. // a Create*Joined() constructor: in this case, it will write the ID
  4692. // of the nested property, and not the PtrInt() transtyped value
  4693. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  4694. end;
  4695. TSQLPropInfoRTTIIDObjArray = array of TSQLPropInfoRTTIID;
  4696. /// information about a TSQLRecord class TStrings/TRawUTF8List/TCollection
  4697. // property
  4698. // - kind sftObject e.g. for TStrings TRawUTF8List TCollection TObjectList instances
  4699. // - binary serialization will store textual JSON serialization of the
  4700. // object, including custom serialization
  4701. TSQLPropInfoRTTIObject = class(TSQLPropInfoRTTIInstance)
  4702. protected
  4703. procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override;
  4704. public
  4705. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  4706. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  4707. var result: RawUTF8; wasSQLString: PBoolean); override;
  4708. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  4709. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  4710. function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override;
  4711. procedure NormalizeValue(var Value: RawUTF8); override;
  4712. procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override;
  4713. end;
  4714. /// information about a TSQLRecord class TSQLRecordMany property
  4715. // - kind sftMany, for which no data is stored in the table itself, but in
  4716. // a separated pivot table
  4717. TSQLPropInfoRTTIMany = class(TSQLPropInfoRTTIInstance)
  4718. public
  4719. procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override;
  4720. procedure GetValueVar(Instance: TObject; ToSQL: boolean;
  4721. var result: RawUTF8; wasSQLString: PBoolean); override;
  4722. procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override;
  4723. function SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar; override;
  4724. end;
  4725. TSQLPropInfoRTTIManyObjArray = array of TSQLPropInfoRTTIMany;
  4726. /// the kind of SQlite3 (virtual) table
  4727. // - TSQLRecordFTS3 will be associated with vFTS3, TSQLRecordFTS4 with vFTS4,
  4728. // TSQLRecordRTree with vRTree, any native SQlite3 table as vSQLite3, and
  4729. // a TSQLRecordVirtualTable*ID with rCustomForcedID/rCustomAutoID
  4730. // - a plain TSQLRecord class can be defined as rCustomForcedID (e.g. for
  4731. // TSQLRecordMany) after registration for an external DB via a call to
  4732. // VirtualTableExternalRegister() from mORMotDB unit
  4733. TSQLRecordVirtualKind = (
  4734. rSQLite3, rFTS3, rFTS4, rRTree, rCustomForcedID, rCustomAutoID);
  4735. /// kind of (static) database server implementation available
  4736. // - sMainEngine will identify the default main SQlite3 engine
  4737. // - sStaticDataTable will identify a TSQLRestStorageInMemory - i.e.
  4738. // TSQLRestServer.fStaticData[] which can work without SQLite3
  4739. // - sVirtualTable will identify virtual TSQLRestStorage classes - i.e.
  4740. // TSQLRestServer.fStaticVirtualTable[] which points to SQLite3 virtual tables
  4741. // (e.g. TObjectList or external databases)
  4742. TSQLRestServerKind = (sMainEngine, sStaticDataTable, sVirtualTable);
  4743. /// pointer to the kind of (static) database server implementation
  4744. PSQLRestServerKind = ^TSQLRestServerKind;
  4745. /// some information about a given TSQLRecord class properties
  4746. // - used internaly by TSQLRecord, via a global cache handled by this unit:
  4747. // you can access to each record's properties via TSQLRecord.RecordProps class
  4748. // - such a global cache saves some memory for each TSQLRecord instance,
  4749. // and allows faster access to most wanted RTTI properties
  4750. TSQLRecordProperties = class
  4751. protected
  4752. fTable: TSQLRecordClass;
  4753. fClassType: PClassType;
  4754. fClassProp: PClassProp;
  4755. fHasNotSimpleFields: boolean;
  4756. fHasTypeFields: TSQLFieldTypes;
  4757. fFields: TSQLPropInfoList;
  4758. fSimpleFields: TSQLPropInfoObjArray;
  4759. fSQLTableName: RawUTF8;
  4760. fCopiableFields: TSQLPropInfoObjArray;
  4761. fManyFields: TSQLPropInfoRTTIManyObjArray;
  4762. fJoinedFields: TSQLPropInfoRTTIIDObjArray;
  4763. fJoinedFieldsTable: TSQLRecordClassDynArray;
  4764. fDynArrayFields: TSQLPropInfoRTTIDynArrayObjArray;
  4765. fDynArrayFieldsHasObjArray: boolean;
  4766. fBlobCustomFields: TSQLPropInfoObjArray;
  4767. fBlobFields: TSQLPropInfoRTTIObjArray;
  4768. fFilters: TSynFilterOrValidateObjArrayArray;
  4769. fRecordManySourceProp: TSQLPropInfoRTTIInstance;
  4770. fRecordManyDestProp: TSQLPropInfoRTTIInstance;
  4771. fSQLTableNameUpperWithDot: RawUTF8;
  4772. fSQLFillPrepareMany: RawUTF8;
  4773. fSQLTableSimpleFieldsNoRowID: RawUTF8;
  4774. fSQLTableUpdateBlobFields: RawUTF8;
  4775. fSQLTableRetrieveBlobFields: RawUTF8;
  4776. fSQLTableRetrieveAllFields: RawUTF8;
  4777. fRecordVersionField: TSQLPropInfoRTTIRecordVersion;
  4778. fWeakZeroClass: TObject;
  4779. /// the associated TSQLModel instances
  4780. // - e.g. allow O(1) search of a TSQLRecordClass in a model
  4781. fModel: array of record
  4782. /// one associated model
  4783. Model: TSQLModel;
  4784. /// the index in the Model.Tables[] array
  4785. TableIndex: integer;
  4786. /// associated ORM parameters
  4787. Properties: TSQLModelRecordProperties;
  4788. end;
  4789. fLock: TRTLCriticalSection;
  4790. fModelMax: integer;
  4791. fCustomCollation: TRawUTF8DynArray;
  4792. /// add an entry in fModel[] / fModelMax
  4793. procedure InternalRegisterModel(aModel: TSQLModel;
  4794. aTableIndex: integer; aProperties: TSQLModelRecordProperties);
  4795. public
  4796. /// initialize the properties content
  4797. constructor Create(aTable: TSQLRecordClass);
  4798. /// release associated used memory
  4799. destructor Destroy; override;
  4800. /// return TRUE if the given name is either ID/RowID, either a property name
  4801. function IsFieldName(const PropName: RawUTF8): boolean;
  4802. /// return TRUE if the given name is either ID/RowID, either a property name,
  4803. // or an aggregate function (MAX/MIN/AVG/SUM) on a valid property name
  4804. function IsFieldNameOrFunction(const PropName: RawUTF8): boolean;
  4805. /// set all bits corresponding to the supplied field names
  4806. // - returns TRUE on success, FALSE if any field name is not existing
  4807. function FieldBitsFromRawUTF8(const aFields: array of RawUTF8;
  4808. var Bits: TSQLFieldBits): boolean; overload;
  4809. /// set all bits corresponding to the supplied field names
  4810. // - returns the matching fields set
  4811. function FieldBitsFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldBits; overload;
  4812. /// set all bits corresponding to the supplied CSV field names
  4813. // - returns TRUE on success, FALSE if any field name is not existing
  4814. function FieldBitsFromCSV(const aFieldsCSV: RawUTF8;
  4815. var Bits: TSQLFieldBits): boolean; overload;
  4816. /// set all bits corresponding to the supplied CSV field names, including ID
  4817. // - returns TRUE on success, FALSE if any field name is not existing
  4818. // - this overloaded method would identify ID/RowID field name, and set
  4819. // withID output parameter according to its presence
  4820. // - if aFieldsCSV='*', Bits will contain all simple fields, and withID=true
  4821. function FieldBitsFromCSV(const aFieldsCSV: RawUTF8;
  4822. var Bits: TSQLFieldBits; out withID: boolean): boolean; overload;
  4823. /// set all bits corresponding to the supplied CSV field names
  4824. // - returns the matching fields set
  4825. function FieldBitsFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldBits; overload;
  4826. /// set all simple bits corresponding to the simple fields, excluding some
  4827. // - could be a convenient alternative to FieldBitsFromCSV() if only some
  4828. // fields are to be excluded
  4829. // - returns the matching fields set
  4830. function FieldBitsFromExcludingCSV(const aFieldsCSV: RawUTF8;
  4831. aOccasion: TSQLOccasion=soSelect): TSQLFieldBits;
  4832. /// set all bits corresponding to the supplied BLOB field type information
  4833. // - returns TRUE on success, FALSE if blob field is not recognized
  4834. function FieldBitsFromBlobField(aBlobField: PPropInfo;
  4835. var Bits: TSQLFieldBits): boolean;
  4836. /// compute the CSV field names text from a set of bits
  4837. function CSVFromFieldBits(const Bits: TSQLFieldBits): RawUTF8;
  4838. /// set all field indexes corresponding to the supplied field names
  4839. // - returns TRUE on success, FALSE if any field name is not existing
  4840. function FieldIndexDynArrayFromRawUTF8(const aFields: array of RawUTF8;
  4841. var Indexes: TSQLFieldIndexDynArray): boolean; overload;
  4842. /// set all field indexes corresponding to the supplied field names
  4843. // - returns the matching fields set
  4844. function FieldIndexDynArrayFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldIndexDynArray; overload;
  4845. /// set all field indexes corresponding to the supplied CSV field names
  4846. // - returns TRUE on success, FALSE if any field name is not existing
  4847. function FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8;
  4848. var Indexes: TSQLFieldIndexDynArray): boolean; overload;
  4849. /// set all field indexes corresponding to the supplied CSV field names
  4850. // - returns the matching fields set
  4851. function FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldIndexDynArray; overload;
  4852. /// set all field indexes corresponding to the supplied BLOB field type information
  4853. // - returns TRUE on success, FALSE if blob field is not recognized
  4854. function FieldIndexDynArrayFromBlobField(aBlobField: PPropInfo;
  4855. var Indexes: TSQLFieldIndexDynArray): boolean;
  4856. /// retrieve a Field property RTTI information from a Property Name
  4857. // - this version returns nil if the property is not a BLOB field
  4858. function BlobFieldPropFromRawUTF8(const PropName: RawUTF8): PPropInfo;
  4859. /// retrieve a Field property RTTI information from a Property Name
  4860. // - this version returns nil if the property is not a BLOB field
  4861. function BlobFieldPropFromUTF8(PropName: PUTF8Char; PropNameLen: integer): PPropInfo;
  4862. /// append a field name to a RawUTF8 Text buffer
  4863. // - if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN (-1), appends 'RowID' or
  4864. // 'ID' (if ForceNoRowID=TRUE) to Text
  4865. // - on error (i.e. if FieldIndex is out of range) will return TRUE
  4866. // - otherwise, will return FALSE and append the field name to Text
  4867. function AppendFieldName(FieldIndex: Integer; var Text: RawUTF8; ForceNoRowID: boolean): boolean;
  4868. /// return the first unique property of kind RawUTF8
  4869. // - this property is mainly the "Name" property, i.e. the one with
  4870. // "stored AS_UNIQUE" (i.e. "stored false") definition on most TSQLRecord
  4871. // - if ReturnFirstIfNoUnique is TRUE and no unique property is found,
  4872. // the first RawUTF8 property is returned anyway
  4873. // - returns '' if no matching field was found
  4874. function MainFieldName(ReturnFirstIfNoUnique: boolean=false): RawUTF8;
  4875. /// return the SQLite3 field datatype for each specified field
  4876. // - set to '' for fields with no column created in the database (e.g. sftMany)
  4877. // - returns e.g. ' INTEGER, ' or ' TEXT COLLATE SYSTEMNOCASE, '
  4878. function SQLFieldTypeToSQL(FieldIndex: integer): RawUTF8;
  4879. /// set a custom SQlite3 text column collation for a specified field
  4880. // - can be used e.g. to override the default COLLATE SYSTEMNOCASE of RawUTF8
  4881. // - collations defined within our SynSQLite3 unit are named BINARY, NOCASE,
  4882. // RTRIM and our custom SYSTEMNOCASE, ISO8601, WIN32CASE, WIN32NOCASE
  4883. // - do nothing if FieldIndex is not valid, and returns false
  4884. // - to be set in overridden class procedure
  4885. // TSQLRecord.InternalRegisterCustomProperties() so that it will be common
  4886. // to all database models, for both client and server
  4887. function SetCustomCollation(FieldIndex: integer; const aCollationName: RawUTF8): boolean; overload;
  4888. /// set a custom SQlite3 text column collation for a specified field
  4889. // - overloaded method which expects the field to be named
  4890. function SetCustomCollation(const aFieldName, aCollationName: RawUTF8): boolean; overload;
  4891. /// set a custom SQlite3 text column collation for a given field type
  4892. // - can be used e.g. to override ALL default COLLATE SYSTEMNOCASE of RawUTF8,
  4893. // or the default COLLATE ISO8601 of TDateTime, and let the generated SQLite3
  4894. // file be available outside the scope of mORMot's SQLite3 engine
  4895. // - collations defined within our SynSQLite3 unit are named BINARY, NOCASE,
  4896. // RTRIM and our custom SYSTEMNOCASE, ISO8601, WIN32CASE, WIN32NOCASE
  4897. // - to be set in overridden class procedure InternalRegisterCustomProperties()
  4898. // so that it will be common to all database models, for both client and server
  4899. procedure SetCustomCollationForAll(aFieldType: TSQLFieldType;
  4900. const aCollationName: RawUTF8);
  4901. /// allow to validate length of all text published properties of this table
  4902. // - the "index" attribute of the RawUTF8/string published properties could
  4903. // be used to specify a maximum length for external VARCHAR() columns
  4904. // - SQLite3 will just ignore this "index" information, but it could be
  4905. // handy to be able to validate the value length before sending to the DB
  4906. // - this method will create TSynValidateText corresponding to the maximum
  4907. // field size specified by the "index" attribute, to validate before write
  4908. // - will expect the "index" value to be in UTF-16 codepoints, unless
  4909. // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length in "index"
  4910. procedure SetMaxLengthValidatorForTextFields(IndexIsUTF8Length: boolean=false);
  4911. /// allow to filter the length of all text published properties of this table
  4912. // - the "index" attribute of the RawUTF8/string published properties could
  4913. // be used to specify a maximum length for external VARCHAR() columns
  4914. // - SQLite3 will just ignore this "index" information, but it could be
  4915. // handy to be able to filter the value length before sending to the DB
  4916. // - this method will create TSynFilterTruncate corresponding to the maximum
  4917. // field size specified by the "index" attribute, to filter before write
  4918. // - will expect the "index" value to be in UTF-16 codepoints, unless
  4919. // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length in "index"
  4920. procedure SetMaxLengthFilterForTextFields(IndexIsUTF8Length: boolean=false);
  4921. {$ifndef NOVARIANTS}
  4922. /// customize the TDocVariant options for all variant published properties
  4923. // - will change the TSQLPropInfoRTTIVariant.DocVariantOptions value
  4924. // - use e.g. as SetVariantFieldDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED)
  4925. procedure SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions);
  4926. {$endif}
  4927. /// return the UTF-8 encoded SQL statement source to alter the table for
  4928. // adding the specified field
  4929. function SQLAddField(FieldIndex: integer): RawUTF8;
  4930. /// create a TJSONWriter, ready to be filled with TSQLRecord.GetJSONValues(W)
  4931. // - you can use TSQLRecordProperties.FieldBitsFromCSV() or
  4932. // TSQLRecordProperties.FieldBitsFromRawUTF8() to compute aFields
  4933. function CreateJSONWriter(JSON: TStream; Expand: boolean; withID: boolean;
  4934. const aFields: TSQLFieldBits; KnownRowsCount: integer): TJSONSerializer; overload;
  4935. /// create a TJSONWriter, ready to be filled with TSQLRecord.GetJSONValues(W)
  4936. // - you can use TSQLRecordProperties.FieldBitsFromCSV() or
  4937. // TSQLRecordProperties.FieldBitsFromRawUTF8() to compute aFields
  4938. function CreateJSONWriter(JSON: TStream; Expand: boolean; withID: boolean;
  4939. const aFields: TSQLFieldIndexDynArray; KnownRowsCount: integer): TJSONSerializer; overload;
  4940. /// create a TJSONWriter, ready to be filled with TSQLRecord.GetJSONValues(W)
  4941. // - this overloaded method would call FieldBitsFromCSV(aFieldsCSV,bits,withID)
  4942. // to retrieve the bits just like a SELECT (i.e. '*' for simple fields)
  4943. function CreateJSONWriter(JSON: TStream; Expand: boolean;
  4944. const aFieldsCSV: RawUTF8; KnownRowsCount: integer): TJSONSerializer; overload;
  4945. /// set the W.ColNames[] array content + W.AddColumns
  4946. procedure SetJSONWriterColumnNames(W: TJSONSerializer; KnownRowsCount: integer);
  4947. /// save the TSQLRecord RTTI into a binary header
  4948. // - used e.g. by TSQLRestStorageInMemory.SaveToBinary()
  4949. procedure SaveBinaryHeader(W: TFileBufferWriter);
  4950. /// ensure that the TSQLRecord RTTI matches the supplied binary header
  4951. // - used e.g. by TSQLRestStorageInMemory.LoadFromBinary()
  4952. function CheckBinaryHeader(var R: TFileBufferReader): boolean;
  4953. /// convert a JSON array of simple field values into a matching JSON object
  4954. function SaveSimpleFieldsFromJsonArray(var P: PUTF8Char;
  4955. var EndOfObject: AnsiChar; ExtendedJSON: boolean): RawUTF8;
  4956. /// register a custom filter (transformation) or validation rule to
  4957. // the TSQMRecord class for a specified field
  4958. // - this will be used by TSQLRecord.Filter and TSQLRecord.Validate
  4959. // methods (in default implementation)
  4960. // - will return FALSE in case of an invalid field index
  4961. function AddFilterOrValidate(aFieldIndex: integer;
  4962. aFilter: TSynFilterOrValidate): boolean; overload;
  4963. /// register a custom filter (transformation) or validatation to the
  4964. // TSQLRecord class for a specified field
  4965. // - this will be used by TSQLRecord.Filter and TSQLRecord.Validate
  4966. // methods (in default implementation)
  4967. // - will raise an EModelException if the field name does not exist
  4968. procedure AddFilterOrValidate(const aFieldName: RawUTF8;
  4969. aFilter: TSynFilterOrValidate); overload;
  4970. /// add a custom unmanaged fixed-size record property
  4971. // - simple kind of records (i.e. those not containing reference-counted
  4972. // members) do not have RTTI generated, at least in older versions of Delphi
  4973. // - use this method within TSQLRecord.InternalRegisterCustomProperties
  4974. // overridden method to define a custom record property with no
  4975. // reference-counted types within (like strings) - typical use may be TGUID
  4976. // - main parameters are the record size, in bytes, and the property pointer
  4977. // - add an TSQLPropInfoRecordFixedSize instance to the internal list
  4978. // - if aData2Text/aText2Data parameters are not defined, it will fallback
  4979. // to TSQLPropInfo.BinaryToText() simple text Base64 encoding
  4980. // - can be used to override the default TSQLRecord corresponding method:
  4981. // !class procedure TSQLMyRecord.InternalRegisterCustomProperties(
  4982. // ! Props: TSQLRecordProperties);
  4983. // !begin
  4984. // ! Props.RegisterCustomFixedSizeRecordProperty(self,sizeof(TMyRec),'RecField',
  4985. // ! @TSQLMyRecord(nil).fRecField, [], sizeof(TMyRec));
  4986. // !end;
  4987. procedure RegisterCustomFixedSizeRecordProperty(aTable: TClass;
  4988. aRecordSize: cardinal; const aName: RawUTF8; aPropertyPointer: pointer;
  4989. aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer;
  4990. aData2Text: TOnSQLPropInfoRecord2Text=nil;
  4991. aText2Data: TOnSQLPropInfoRecord2Data=nil);
  4992. /// add a custom record property from its RTTI definition
  4993. // - handle any kind of record with TypeInfo() generated
  4994. // - use this method within InternalRegisterCustomProperties overridden method
  4995. // to define a custom record property containing reference-counted types
  4996. // - main parameters are the record RTTI information, and the property pointer
  4997. // - add an TSQLPropInfoRecordRTTI instance to the internal list
  4998. // - can be used as such:
  4999. // !class procedure TSQLMyRecord.InternalRegisterCustomProperties(
  5000. // ! Props: TSQLRecordProperties);
  5001. // !begin
  5002. // ! Props.RegisterCustomRTTIRecordProperty(self,TypeInfo(TMyRec),'RecField',
  5003. // ! @TSQLMyRecord(nil).fRecField);
  5004. // !end;
  5005. procedure RegisterCustomRTTIRecordProperty(aTable: TClass;
  5006. aRecordInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer;
  5007. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0;
  5008. aData2Text: TOnSQLPropInfoRecord2Text=nil;
  5009. aText2Data: TOnSQLPropInfoRecord2Data=nil);
  5010. /// add a custom property from its RTTI definition stored as JSON
  5011. // - handle any kind of record with TypeInfo() generated
  5012. // - use this method within InternalRegisterCustomProperties overridden method
  5013. // to define a custom record property containing reference-counted types
  5014. // - main parameters are the record RTTI information, and the property pointer
  5015. // - add an TSQLPropInfoCustomJSON instance to the internal list
  5016. // - can be used as such:
  5017. // !class procedure TSQLMyRecord.InternalRegisterCustomProperties(
  5018. // ! Props: TSQLRecordProperties);
  5019. // !begin
  5020. // ! Props.RegisterCustomPropertyFromRTTI(self,TypeInfo(TMyRec),'RecField',
  5021. // ! @TSQLMyRecord(nil).fRecField);
  5022. // !end;
  5023. procedure RegisterCustomPropertyFromRTTI(aTable: TClass;
  5024. aTypeInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer;
  5025. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0);
  5026. /// add a custom property from its type name, stored as JSON
  5027. // - handle any kind of registered record, including TGUID
  5028. // - use this method within InternalRegisterCustomProperties overridden method
  5029. // to define a custom record property containing reference-counted types
  5030. // - main parameters are the record RTTI information, and the property pointer
  5031. // - add an TSQLPropInfoCustomJSON instance to the internal list
  5032. // - can be used as such:
  5033. // !class procedure TSQLMyRecord.InternalRegisterCustomProperties(
  5034. // ! Props: TSQLRecordProperties);
  5035. // !begin
  5036. // ! Props.RegisterCustomPropertyFromTypeName(self,'TGUID','GUID',
  5037. // ! @TSQLMyRecord(nil).fGUID,[aIsUnique],38);
  5038. // !end;
  5039. procedure RegisterCustomPropertyFromTypeName(aTable: TClass;
  5040. const aTypeName, aName: RawUTF8; aPropertyPointer: pointer;
  5041. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0);
  5042. /// fast access to the RTTI properties attribute
  5043. property TableClassType: PClassType read fClassType;
  5044. /// fast access to the RTTI properties attribute
  5045. property TableClassProp: PClassProp read fClassProp;
  5046. /// if this class has any BLOB or TSQLRecodMany fields
  5047. // - i.e. some fields to be ignored
  5048. property HasNotSimpleFields: boolean read fHasNotSimpleFields;
  5049. /// set of field types appearing in this record
  5050. property HasTypeFields: TSQLFieldTypes read fHasTypeFields;
  5051. /// list all fields, as retrieved from RTTI
  5052. property Fields: TSQLPropInfoList read fFields;
  5053. /// list all "simple" fields of this TSQLRecord
  5054. // - by default, the TSQLRawBlob and TSQLRecordMany fields are not included
  5055. // into this set: they must be read specificaly (in order to spare
  5056. // bandwidth for BLOBs)
  5057. // - dynamic arrays belong to simple fields: they are sent with other
  5058. // properties content
  5059. // - match inverted NOT_SIMPLE_FIELDS mask
  5060. property SimpleFields: TSQLPropInfoObjArray read fSimpleFields;
  5061. /// list all fields which can be copied from one TSQLRecord instance to another
  5062. // - match COPIABLE_FIELDS mask, i.e. all fields except sftMany
  5063. property CopiableFields: TSQLPropInfoObjArray read fCopiableFields;
  5064. /// list all TSQLRecordMany fields of this TSQLRecord
  5065. property ManyFields: TSQLPropInfoRTTIManyObjArray read fManyFields;
  5066. /// list all TSQLRecord fields of this TSQLRecord
  5067. // - ready to be used by TSQLTableJSON.CreateFromTables()
  5068. // - i.e. the class itself then, all fields of type sftID (excluding sftMany)
  5069. property JoinedFields: TSQLPropInfoRTTIIDObjArray read fJoinedFields;
  5070. /// wrapper of all nested TSQLRecord class of this TSQLRecord
  5071. // - ready to be used by TSQLTableJSON.CreateFromTables()
  5072. // - i.e. the class itself as JoinedFieldsTable[0], then, all nested
  5073. // TSQLRecord published properties (of type sftID, ergo excluding sftMany)
  5074. // - equals nil if there is no nested TSQLRecord property (i.e. JoinedFields=nil)
  5075. property JoinedFieldsTable: TSQLRecordClassDynArray read fJoinedFieldsTable;
  5076. /// list of all sftBlobDynArray fields of this TSQLRecord
  5077. property DynArrayFields: TSQLPropInfoRTTIDynArrayObjArray read fDynArrayFields;
  5078. /// TRUE if any of the sftBlobDynArray fields of this TSQLRecord is a T*ObjArray
  5079. property DynArrayFieldsHasObjArray: boolean read fDynArrayFieldsHasObjArray;
  5080. /// list of all sftBlobCustom fields of this TSQLRecord
  5081. // - have been defined e.g. as TSQLPropInfoCustom custom definition
  5082. property BlobCustomFields: TSQLPropInfoObjArray read fBlobCustomFields;
  5083. /// list all BLOB fields of this TSQLRecord
  5084. // - i.e. generic sftBlob fields (not sftBlobDynArray, sftBlobCustom nor
  5085. // sftBlobRecord)
  5086. property BlobFields: TSQLPropInfoRTTIObjArray read fBlobFields;
  5087. /// all TSynFilter or TSynValidate instances registered per each field
  5088. // - since validation and filtering are used within some CPU-consuming
  5089. // part of the framework (like UI edition), both filters and validation
  5090. // rules are grouped in the same list - for TSynTableFieldProperties there
  5091. // are separated Filters[] and Validates[] arrays, for better performance
  5092. property Filters: TSynFilterOrValidateObjArrayArray read fFilters;
  5093. /// for a TSQLRecordMany class, points to the Source property RTTI
  5094. property RecordManySourceProp: TSQLPropInfoRTTIInstance read fRecordManySourceProp;
  5095. /// for a TSQLRecordMany class, points to the Dest property RTTI
  5096. property RecordManyDestProp: TSQLPropInfoRTTIInstance read fRecordManyDestProp;
  5097. /// points to any TRecordVersion field
  5098. // - contains nil if no such sftRecordVersion field do exist
  5099. // - will be used by low-level storage engine to compute and store the
  5100. // monotonic version number during any write operation
  5101. property RecordVersionField: TSQLPropInfoRTTIRecordVersion read fRecordVersionField;
  5102. /// the Table name in the database in uppercase with a final '.'
  5103. // - e.g. 'TEST.' for TSQLRecordTest class
  5104. // - can be used with IdemPChar() for fast check of a table name
  5105. property SQLTableNameUpperWithDot: RawUTF8 read fSQLTableNameUpperWithDot;
  5106. /// returns 'COL1,COL2' with all COL* set to simple field names
  5107. // - same value as SQLTableSimpleFields[false,false]
  5108. // - this won't change depending on the ORM settings: so it can be safely
  5109. // computed here and not in TSQLModelRecordProperties
  5110. // - used e.g. by TSQLRecord.GetSQLValues
  5111. property SQLTableSimpleFieldsNoRowID: RawUTF8 read fSQLTableSimpleFieldsNoRowID;
  5112. /// returns 'COL1=?,COL2=?' with all BLOB columns names
  5113. // - used e.g. by TSQLRestServerDB.UpdateBlobFields()
  5114. property SQLTableUpdateBlobFields: RawUTF8 read fSQLTableUpdateBlobFields;
  5115. /// returns 'COL1,COL2' with all BLOB columns names
  5116. // - used e.g. by TSQLRestServerDB.RetrieveBlobFields()
  5117. property SQLTableRetrieveBlobFields: RawUTF8 read fSQLTableRetrieveBlobFields;
  5118. public
  5119. /// bit set to 1 for indicating each TSQLFieldType fields of this TSQLRecord
  5120. FieldBits: array[TSQLFieldType] of TSQLFieldBits;
  5121. /// bit set to 1 for indicating TModTime/TSessionUserID fields
  5122. // of this TSQLRecord (leaving TCreateTime untouched)
  5123. // - as applied before an UPDATE
  5124. // - i.e. sftModTime and sftSessionUserID fields
  5125. ComputeBeforeUpdateFieldsBits: TSQLFieldBits;
  5126. /// bit set to 1 for indicating TModTime/TCreateTime/TSessionUserID fields
  5127. // of this TSQLRecord
  5128. // - as applied before an INSERT
  5129. // - i.e. sftModTime, sftCreateTime and sftSessionUserID fields
  5130. ComputeBeforeAddFieldsBits: TSQLFieldBits;
  5131. /// bit set to 1 for indicating fields to export, i.e. "simple" fields
  5132. // - this array will handle special cases, like the TCreateTime fields
  5133. // which shall not be included in soUpdate but soInsert and soSelect e.g.
  5134. SimpleFieldsBits: array[TSQLOccasion] of TSQLFieldBits;
  5135. /// number of fields to export, i.e. "simple" fields
  5136. // - this array will handle special cases, like the TCreateTime fields
  5137. // which shall not be included in soUpdate but soInsert and soSelect e.g.
  5138. SimpleFieldsCount: array[TSQLOccasion] of integer;
  5139. /// bit set to 1 for an unique field
  5140. // - an unique field is defined as "stored AS_UNIQUE" (i.e. "stored false")
  5141. // in its property definition
  5142. IsUniqueFieldsBits: TSQLFieldBits;
  5143. /// bit set to 1 for the smallest simple fields
  5144. // - i.e. excluding non only sftBlob and sftMany, but also sftVariant,
  5145. // sftBlobDynArray, sftBlobCustom and sftUTF8Custom fields
  5146. // - may be used to minimize the transmitted content, e.g. when serializing
  5147. // to JSON for the most
  5148. SmallFieldsBits: TSQLFieldBits;
  5149. /// bit set to 1 for the all fields storing some data
  5150. // - match COPIABLE_FIELDS mask, i.e. all fields except sftMany
  5151. CopiableFieldsBits: TSQLFieldBits;
  5152. /// contains the main field index (e.g. mostly 'Name')
  5153. // - the [boolean] is for [ReturnFirstIfNoUnique] version
  5154. // - contains -1 if no field matches
  5155. MainField: array[boolean] of integer;
  5156. published
  5157. /// the TSQLRecord class
  5158. property Table: TSQLRecordClass read fTable;
  5159. /// the Table name in the database, associated with this TSQLRecord class
  5160. // - 'TSQL' or 'TSQLRecord' chars are trimmed at the beginning of the ClassName
  5161. // - or the ClassName is returned as is, if no 'TSQL' or 'TSQLRecord' at first
  5162. property SQLTableName: RawUTF8 read fSQLTableName;
  5163. /// returns 'COL1,COL2' with all COL* set to all field names, including
  5164. // RowID, TRecordVersion and BLOBs
  5165. // - this won't change depending on the ORM settings: so it can be safely
  5166. // computed here and not in TSQLModelRecordProperties
  5167. // - used e.g. by TSQLRest.InternalListJSON()
  5168. property SQLTableRetrieveAllFields: RawUTF8 read fSQLTableRetrieveAllFields;
  5169. end;
  5170. TServiceFactoryServer = class;
  5171. PSQLAccessRights = ^TSQLAccessRights;
  5172. /// flags which may be set by the caller to notify low-level context
  5173. // - llfSSL will indicates that the communication was made over HTTPS
  5174. TSQLRestURIParamsLowLevelFlag = (llfSSL);
  5175. /// some flags set by the caller to notify low-level context
  5176. TSQLRestURIParamsLowLevelFlags = set of TSQLRestURIParamsLowLevelFlag;
  5177. /// store all parameters for a TSQLRestServer.URI() method call
  5178. // - see TSQLRestClient to check how data is expected in our RESTful format
  5179. TSQLRestURIParams = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  5180. /// input parameter containing the caller URI
  5181. Url: RawUTF8;
  5182. /// input parameter containing the caller method
  5183. // - handle enhanced REST codes: LOCK/UNLOCK/BEGIN/END/ABORT
  5184. Method: RawUTF8;
  5185. /// input parameter containing the caller message headers
  5186. // - you can use e.g.
  5187. // ! FindIniNameValue(pointer(Call.InHead),HEADER_CONTENT_TYPE_UPPER)
  5188. // to retrieve the incoming message body content type
  5189. // - or to retrieve the remote IP
  5190. // ! FindIniNameValue(pointer(Call.InHead),'REMOTEIP: ')
  5191. // - but consider also using TSQLRestServerURIContext.InHeader['remoteip']
  5192. InHead: RawUTF8;
  5193. /// input parameter containing the caller message body
  5194. // - e.g. some GET/POST/PUT JSON data can be specified here
  5195. InBody: RawUTF8;
  5196. /// output parameter to be set to the response message header
  5197. // - it is the right place to set the returned message body content type,
  5198. // e.g. TEXT_CONTENT_TYPE_HEADER or HTML_CONTENT_TYPE_HEADER: if not set,
  5199. // the default JSON_CONTENT_TYPE_HEADER will be returned to the client,
  5200. // meaning that the message is JSON
  5201. // - you can use OutBodyType() function to retrieve the stored content-type
  5202. OutHead: RawUTF8;
  5203. /// output parameter to be set to the response message body
  5204. OutBody: RawUTF8;
  5205. /// output parameter to be set to the HTTP status integer code
  5206. // - HTML_NOTFOUND=404 e.g. if the url doesn't start with Model.Root (caller
  5207. // can try another TSQLRestServer)
  5208. OutStatus: cardinal;
  5209. /// output parameter to be set to the database internal state
  5210. OutInternalState: cardinal;
  5211. /// associated RESTful access rights
  5212. // - AccessRights must be handled by the TSQLRestServer child, according
  5213. // to the Application Security Policy (user logging, authentification and
  5214. // rights management) - making access rights a parameter allows this method
  5215. // to be handled as pure stateless, thread-safe and session-free
  5216. RestAccessRights: PSQLAccessRights;
  5217. /// opaque reference to the protocol context which made this request
  5218. // - may point e.g. to a THttpServerResp, a TWebSocketServerResp,
  5219. // a THttpApiServer, a TSQLRestClientURI, a TFastCGIServer or a
  5220. // TSQLRestServerNamedPipeResponse instance
  5221. // - is a Int64 as expected by http.sys, but is an incremental sequence
  5222. // of integer for THttpServer/TWebSocketServer, or a PtrInt(self)
  5223. LowLevelConnectionID: Int64;
  5224. /// low-level properties of the current protocol context
  5225. LowLevelFlags: TSQLRestURIParamsLowLevelFlags;
  5226. /// initialize the non RawUTF8 values
  5227. procedure Init; overload;
  5228. /// initialize the input values
  5229. procedure Init(const aURI,aMethod,aInHead,aInBody: RawUTF8); overload;
  5230. /// retrieve the "Content-Type" value from InHead
  5231. // - if GuessJSONIfNoneSet is TRUE, returns JSON if none was set in headers
  5232. function InBodyType(GuessJSONIfNoneSet: boolean=True): RawUTF8;
  5233. /// retrieve the "Content-Type" value from OutHead
  5234. // - if GuessJSONIfNoneSet is TRUE, returns JSON if none was set in headers
  5235. function OutBodyType(GuessJSONIfNoneSet: boolean=True): RawUTF8;
  5236. end;
  5237. /// used to map set of parameters for a TSQLRestServer.URI() method
  5238. PSQLRestURIParams = ^TSQLRestURIParams;
  5239. /// points to the currently running service on the server side
  5240. // - your code may use such a local pointer to retrieve the ServiceContext
  5241. // threadvar once in a method, since threadvar access does cost some CPU
  5242. // !var context: PServiceRunningContext;
  5243. // !begin
  5244. // ! context := @ServiceContext; // threadvar access once
  5245. // ! ...
  5246. PServiceRunningContext = ^TServiceRunningContext;
  5247. TSQLRestServerURIContext = class;
  5248. TAuthSession = class;
  5249. /// used to identify the authentication failure reason
  5250. // - as transmitted e.g. by TSQLRestServerURIContext.AuthenticationFailed or
  5251. // TSQLRestServer.OnAuthenticationFailed
  5252. TNotifyAuthenticationFailedReason = (
  5253. afInvalidSignature,afRemoteServiceExecutionNotAllowed,
  5254. afUnknownUser,afInvalidPassword,
  5255. afSessionAlreadyStartedForThisUser,afSessionCreationAborted);
  5256. /// will identify the currently running service on the server side
  5257. // - is the type of the global ServiceContext threadvar
  5258. // - to access the current TSQLRestServer instance (and e.g. its ORM/CRUD
  5259. // or SOA methods), use Request.Server and not Factory.Server, which may not
  5260. // be available e.g. if you run the service from the server side (so no
  5261. // factory is involved)
  5262. // - note that the safest (and slightly faster) access to the TSQLRestServer
  5263. // instance associated with a service is to inherit your implementation
  5264. // class from TInjectableObjectRest
  5265. TServiceRunningContext = record
  5266. /// the currently running service factory
  5267. // - it can be used within server-side implementation to retrieve the
  5268. // associated TSQLRestServer instance
  5269. // - note that TServiceFactoryServer.Get() won't override this value, when
  5270. // called within another service (i.e. if Factory is not nil)
  5271. Factory: TServiceFactoryServer;
  5272. /// the currently runnning context which launched the method
  5273. // - low-level RESTful context is also available in its Call member
  5274. // - Request.Server is the safe access point to the underlying TSQLRestServer,
  5275. // unless the service is implemented via TInjectableObjectRest, so the
  5276. // TInjectableObjectRest.Server property is preferred
  5277. // - make available e.g. current session or authentication parameters
  5278. // (including e.g. user details via Request.Server.SessionGetUser)
  5279. Request: TSQLRestServerURIContext;
  5280. /// the thread which launched the request
  5281. // - is set by TSQLRestServer.BeginCurrentThread from multi-thread server
  5282. // handlers - e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse
  5283. RunningThread: TThread;
  5284. end;
  5285. /// possible service provider method options, e.g. about logging or execution
  5286. // - see TServiceMethodOptions for a description of each available option
  5287. TServiceMethodOption = (
  5288. optExecLockedPerInterface,
  5289. optExecInPerInterfaceThread, optFreeInPerInterfaceThread,
  5290. {$ifndef LVCL}
  5291. optExecInMainThread, optFreeInMainThread,
  5292. optVariantCopiedByReference, optInterceptInputOutput,
  5293. {$endif}
  5294. optNoLogInput, optNoLogOutput, optErrorOnMissingParam
  5295. );
  5296. /// set of per-method execution options for an interface-based service provider
  5297. // - by default, mehthod executions are concurrent, for better server
  5298. // responsiveness; if you set optExecLockedPerInterface, all methods of
  5299. // a given interface will be executed with a critical section
  5300. // - optExecInMainThread will force the method to be called within
  5301. // a RunningThread.Synchronize() call - it can be used e.g. if your
  5302. // implementation rely heavily on COM servers - by default, service methods
  5303. // are called within the thread which received them, on multi-thread server
  5304. // instances (e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse),
  5305. // for better response time and CPU use (this is the technical reason why
  5306. // service implementation methods have to handle multi-threading safety
  5307. // carefully, e.g. by using TRTLCriticalSection mutex on purpose)
  5308. // - optFreeInMainThread will force the _Release/Destroy method to be run
  5309. // in the main thread: setting this option for any method will affect the
  5310. // whole service class - is not set by default, for performance reasons
  5311. // - optExecInPerInterfaceThread and optFreeInPerInterfaceThread will allow
  5312. // creation of a per-interface dedicated thread
  5313. // - if optInterceptInputOutput is set, TServiceFactoryServer.AddInterceptor()
  5314. // events would have their Sender.Input/Output values defined
  5315. // - if optNoLogInput/optNoLogOutput is set, TSynLog and ServiceLog() database
  5316. // won't log any parameter values at input/output - this may be useful for
  5317. // regulatory/safety purposes, e.g. to ensure that no sensitive information
  5318. // (like a credit card number or a password), is logged during process
  5319. // - when parameters are transmitted as JSON object, any missing parameter
  5320. // would be replaced by their default value, unless optErrorOnMissingParam
  5321. // is defined to reject the call
  5322. TServiceMethodOptions = set of TServiceMethodOption;
  5323. /// internal per-method list of execution context as hold in TServiceFactory
  5324. TServiceFactoryExecution = record
  5325. /// the list of denied TSQLAuthGroup ID(s)
  5326. // - used on server side within TSQLRestServerURIContext.ExecuteSOAByInterface
  5327. // - bit 0 for client TSQLAuthGroup.ID=1 and so on...
  5328. // - is therefore able to store IDs up to 256
  5329. // - void by default, i.e. no denial = all groups allowed for this method
  5330. Denied: set of 0..255;
  5331. /// execution options for this method (about thread safety or logging)
  5332. Options: TServiceMethodOptions;
  5333. /// where execution information should be written as TSQLRecordServiceLog
  5334. LogRest: TSQLRest;
  5335. /// the TSQLRecordServiceLog class to use, as defined in LogRest.Model
  5336. LogClassModelIndex: integer;
  5337. /// curent BATCH instance used to write on LogRest
  5338. // - points to a TServiceFactoryServer.fLogRestBatch[] instance
  5339. LogRestBatch: TSQLRestBatchLocked;
  5340. end;
  5341. /// points to the execution context of one method within TServiceFactory
  5342. PServiceFactoryExecution = ^TServiceFactoryExecution;
  5343. /// all commands which may be executed by TSQLRestServer.URI() method
  5344. // - execSOAByMethod for method-based services
  5345. // - execSOAByInterface for interface-based services
  5346. // - execORMGet for ORM reads i.e. Retrieve*() methods
  5347. // - execORMWrite for ORM writes i.e. Add Update Delete TransactionBegin
  5348. // Commit Rollback methods
  5349. TSQLRestServerURIContextCommand = (
  5350. execNone, execSOAByMethod, execSOAByInterface, execORMGet, execORMWrite);
  5351. /// class used to define the Client-Server expected routing
  5352. // - most of the internal methods are declared as virtual, so it allows any
  5353. // kind of custom routing or execution scheme
  5354. // - TSQLRestRoutingREST and TSQLRestRoutingJSON_RPC classes
  5355. // are provided in this unit, to allow RESTful and JSON/RPC protocols
  5356. TSQLRestServerURIContextClass = class of TSQLRestServerURIContext;
  5357. /// a set of potential actions to be executed from the server
  5358. // - reSQL will indicate the right to execute any POST SQL statement (not only
  5359. // SELECT statements)
  5360. // - reSQLSelectWithoutTable will allow executing a SELECT statement with
  5361. // arbitrary content via GET/LOCK (simple SELECT .. FROM aTable will be checked
  5362. // against TSQLAccessRights.GET[] per-table right
  5363. // - reService will indicate the right to execute the interface-based JSON-RPC
  5364. // service implementation
  5365. // - reUrlEncodedSQL will indicate the right to execute a SQL query encoded
  5366. // at the URI level, for a GET (to be used e.g. with XMLHTTPRequest, which
  5367. // forced SentData='' by definition), encoded as sql=.... inline parameter
  5368. // - reUrlEncodedDelete will indicate the right to delete items using a
  5369. // WHERE clause for DELETE verb at /root/TableName?WhereClause
  5370. // - reOneSessionPerUser will force that only one session may be created
  5371. // for one user, even if connection comes from the same IP: in this case,
  5372. // you may have to set the SessionTimeOut to a small value, in case the
  5373. // session is not closed gracefully
  5374. // - by default, read/write access to the TSQLAuthUser table is disallowed,
  5375. // for obvious security reasons: but you can define reUserCanChangeOwnPassword
  5376. // so that the current logged user would be able to change its own password
  5377. // - order of this set does matter, since it will be stored as a byte value
  5378. // e.g. by TSQLAccessRights.ToString: ensure that new items would always be
  5379. // appended to the list, not inserted within
  5380. TSQLAllowRemoteExecute = set of (
  5381. reSQL, reService, reUrlEncodedSQL, reUrlEncodedDelete, reOneSessionPerUser,
  5382. reSQLSelectWithoutTable, reUserCanChangeOwnPassword);
  5383. /// set the User Access Rights, for each Table
  5384. // - one property for every and each URI method (GET/POST/PUT/DELETE)
  5385. // - one bit for every and each Table in Model.Tables[]
  5386. {$ifndef ISDELPHI2010}
  5387. TSQLAccessRights = object
  5388. {$else}
  5389. TSQLAccessRights = record
  5390. {$endif}
  5391. /// set of allowed actions on the server side
  5392. AllowRemoteExecute: TSQLAllowRemoteExecute;
  5393. /// GET method (retrieve record) table access bits
  5394. // - note that a GET request with a SQL statement without a table (i.e.
  5395. // on 'ModelRoot' URI with a SQL statement as SentData, as used in
  5396. // TSQLRestClientURI.UpdateFromServer) will be checked for simple cases
  5397. // (i.e. the first table in the FROM clause), otherwise will follow , whatever the bits
  5398. // here are: since TSQLRestClientURI.UpdateFromServer() is called only
  5399. // for refreshing a direct statement, it will be OK; you can improve this
  5400. // by overriding the TSQLRestServer.URI() method
  5401. // - if the REST request is LOCK, the PUT access bits will be read instead
  5402. // of the GET bits value
  5403. GET: TSQLFieldTables;
  5404. /// POST method (create record) table access bits
  5405. POST: TSQLFieldTables;
  5406. /// PUT method (update record) table access bits
  5407. // - if the REST request is LOCK, the PUT access bits will be read instead
  5408. // of the GET bits value
  5409. PUT: TSQLFieldTables;
  5410. /// DELETE method (delete record) table access bits
  5411. DELETE: TSQLFieldTables;
  5412. /// wrapper method which can be used to set the CRUD abilities over a table
  5413. // - C=Create, R=Read, U=Update, D=Delete rights
  5414. procedure Edit(aTableIndex: integer; C, R, U, D: Boolean); overload;
  5415. /// wrapper method which can be used to set the CRUD abilities over a table
  5416. // - use TSQLOccasion set as parameter
  5417. procedure Edit(aTableIndex: integer; aRights: TSQLOccasions); overload;
  5418. /// wrapper method which can be used to set the CRUD abilities over a table
  5419. // - will raise an EModelException if the supplied table is incorrect
  5420. // - C=Create, R=Read, U=Update, D=Delete rights
  5421. procedure Edit(aModel: TSQLModel; aTable: TSQLRecordClass; C, R, U, D: Boolean); overload;
  5422. /// wrapper method which can be used to set the CRUD abilities over a table
  5423. // - will raise an EModelException if the supplied table is incorrect
  5424. // - use TSQLOccasion set as parameter
  5425. procedure Edit(aModel: TSQLModel; aTable: TSQLRecordClass; aRights: TSQLOccasions); overload;
  5426. /// serialize the content as TEXT
  5427. // - use the TSQLAuthGroup.AccessRights CSV format
  5428. function ToString: RawUTF8;
  5429. /// unserialize the content from TEXT
  5430. // - use the TSQLAuthGroup.AccessRights CSV format
  5431. procedure FromString(P: PUTF8Char);
  5432. /// validate mPost/mPut/mDelete action against those access rights
  5433. // - used by TSQLRestServerURIContext.ExecuteORMWrite and
  5434. // TSQLRestServer.EngineBatchSend methods for proper security checks
  5435. function CanExecuteORMWrite(Method: TSQLURIMethod;
  5436. Table: TSQLRecordClass; TableIndex: integer; const TableID: TID;
  5437. Context: TSQLRestServerURIContext): boolean;
  5438. end;
  5439. /// used by TSQLRestServerURIContext.ClientKind to identify the
  5440. // currently connected client
  5441. TSQLRestServerURIContextClientKind = (ckUnknown, ckFramework, ckAJAX);
  5442. /// abstract calling context for a TSQLRestServerCallBack event handler
  5443. // - having a dedicated class avoid changing the implementation methods
  5444. // signature if the framework add some parameters or behavior to it
  5445. // - see TSQLRestServerCallBack for general code use
  5446. // - most of the internal methods are declared as virtual, so it allows any
  5447. // kind of custom routing or execution scheme
  5448. // - instantiated by the TSQLRestServer.URI() method using its ServicesRouting
  5449. // property
  5450. // - see TSQLRestRoutingREST and TSQLRestRoutingJSON_RPC
  5451. // for overridden methods - NEVER set this abstract TSQLRestServerURIContext
  5452. // class on TSQLRest.ServicesRouting property !
  5453. TSQLRestServerURIContext = class
  5454. protected
  5455. fInput: TRawUTF8DynArray; // even items are parameter names, odd are values
  5456. fInputPostContentType: RawUTF8;
  5457. fInputCookiesRetrieved: boolean;
  5458. fInputCookies: TRawUTF8DynArray; // only computed if InCookie[] is used
  5459. fInputCookieLastName: RawUTF8;
  5460. fInputCookieLastValue: RawUTF8;
  5461. fOutSetCookie: RawUTF8;
  5462. fUserAgent: RawUTF8;
  5463. fAuthSession: TAuthSession;
  5464. fServiceListInterfaceMethodIndex: integer;
  5465. fClientKind: TSQLRestServerURIContextClientKind;
  5466. // just a wrapper over @ServiceContext threadvar
  5467. fThreadServer: PServiceRunningContext;
  5468. fSessionAccessRights: TSQLAccessRights; // session may be deleted meanwhile
  5469. {$ifndef NOVARIANTS}
  5470. function GetInput(const ParamName: RawUTF8): variant;
  5471. function GetInputOrVoid(const ParamName: RawUTF8): variant;
  5472. function GetInputAsTDocVariant: variant;
  5473. {$endif}
  5474. function GetInputNameIndex(const ParamName: RawUTF8): integer;
  5475. function GetInputExists(const ParamName: RawUTF8): Boolean;
  5476. function GetInputInt(const ParamName: RawUTF8): Int64;
  5477. function GetInputDouble(const ParamName: RawUTF8): Double;
  5478. function GetInputUTF8(const ParamName: RawUTF8): RawUTF8;
  5479. function GetInputString(const ParamName: RawUTF8): string;
  5480. function GetInputIntOrVoid(const ParamName: RawUTF8): Int64;
  5481. function GetInputHexaOrVoid(const ParamName: RawUTF8): cardinal;
  5482. function GetInputDoubleOrVoid(const ParamName: RawUTF8): Double;
  5483. function GetInputUTF8OrVoid(const ParamName: RawUTF8): RawUTF8;
  5484. function GetInputStringOrVoid(const ParamName: RawUTF8): string;
  5485. function GetInHeader(const HeaderName: RawUTF8): RawUTF8;
  5486. function GetInCookie(CookieName: RawUTF8): RawUTF8;
  5487. procedure SetInCookie(CookieName, CookieValue: RawUTF8);
  5488. function GetUserAgent: RawUTF8;
  5489. function GetResourceFileName: TFileName;
  5490. procedure SetOutSetCookie(aOutSetCookie: RawUTF8);
  5491. procedure ServiceResultStart(WR: TTextWriter); virtual;
  5492. procedure ServiceResultEnd(WR: TTextWriter; ID: TID); virtual;
  5493. procedure InternalSetTableFromTableIndex(Index: integer); virtual;
  5494. procedure InternalSetTableFromTableName(TableName: PUTF8Char); virtual;
  5495. procedure InternalExecuteSOAByInterface; virtual;
  5496. procedure StatsFromContext(Stats: TSynMonitorInputOutput;
  5497. var Diff: Int64; DiffIsMicroSecs: boolean);
  5498. /// event raised by ExecuteMethod() for interface parameters
  5499. // - match TServiceMethodInternalExecuteCallback signature
  5500. procedure ExecuteCallback(var Par: PUTF8Char; ParamInterfaceInfo: PTypeInfo;
  5501. out Obj);
  5502. /// initialize the execution context
  5503. // - this method has been declared as protected, since it shuold never be
  5504. // called outside the TSQLRestServer.URI() method workflow
  5505. // - should set Call, and Method members
  5506. constructor Create(aServer: TSQLRestServer; const aCall: TSQLRestURIParams); virtual;
  5507. /// retrieve RESTful URI routing
  5508. // - should set URI, Table,TableIndex,TableRecordProps,TableEngine,
  5509. // ID, URIBlobFieldName and Parameters members
  5510. // - all Table* members will be set via a InternalSetTableFromTableName() call
  5511. // - default implementation expects an URI encoded with
  5512. // 'ModelRoot[/TableName[/TableID][/BlobFieldName]][?param=...]' format
  5513. // - will also set URISessionSignaturePos and URIWithoutSignature members
  5514. // - return FALSE in case of incorrect URI (e.g. does not match Model.Root)
  5515. function URIDecodeREST: boolean; virtual;
  5516. /// retrieve method-based SOA URI routing with optional RESTful mode
  5517. // - should set MethodIndex member
  5518. // - default RESTful implementation expects an URI encoded with
  5519. // 'ModelRoot/MethodName' or 'ModelRoot/TableName[/TableID]/MethodName' formats
  5520. procedure URIDecodeSOAByMethod; virtual;
  5521. /// retrieve interface-based SOA
  5522. // - should set Service member (and possibly ServiceMethodIndex)
  5523. // - abstract implementation which is to be overridden
  5524. procedure URIDecodeSOAByInterface; virtual; abstract;
  5525. /// process authentication
  5526. // - return FALSE in case of invalid signature, TRUE if authenticated
  5527. function Authenticate: boolean; virtual;
  5528. /// method called in case of authentication failure
  5529. // - the failure origin is stated by the Reason parameter
  5530. // - this default implementation will just set OutStatus := HTML_FORBIDDEN
  5531. // and call TSQLRestServer.OnAuthenticationFailed event (if any)
  5532. procedure AuthenticationFailed(Reason: TNotifyAuthenticationFailedReason); virtual;
  5533. /// direct launch of a method-based service
  5534. // - URI() will ensure that MethodIndex>=0 before calling it
  5535. procedure ExecuteSOAByMethod; virtual;
  5536. /// direct launch of an interface-based service
  5537. // - URI() will ensure that Service<>nil before calling it
  5538. // - abstract implementation which is to be overridden
  5539. procedure ExecuteSOAByInterface; virtual; abstract;
  5540. /// handle GET/LOCK/UNLOCK/STATE verbs for ORM/CRUD process
  5541. procedure ExecuteORMGet; virtual;
  5542. /// handle POST/PUT/DELETE/BEGIN/END/ABORT verbs for ORM/CRUD process
  5543. // - execution of this method is protected by a critical section
  5544. procedure ExecuteORMWrite; virtual;
  5545. /// launch the Execute* method in the execution mode
  5546. // set by Server.AcquireExecutionMode/AcquireExecutionLockedTimeOut
  5547. // - this is the main process point from TSQLRestServer.URI()
  5548. procedure ExecuteCommand;
  5549. public
  5550. /// the associated TSQLRestServer instance which executes its URI method
  5551. Server: TSQLRestServer;
  5552. /// the used Client-Server method (matching the corresponding HTTP Verb)
  5553. // - this property will be set from incoming URI, even if RESTful
  5554. // authentication is not enabled
  5555. Method: TSQLURIMethod;
  5556. /// the URI address, excluding ?par1=.... parameters
  5557. // - can be either the table name (in RESTful protocol), or a service name
  5558. URI: RawUTF8;
  5559. /// same as URI, but without the &session_signature=... ending
  5560. URIWithoutSignature: RawUTF8;
  5561. /// the optional Blob field name as specified in URI
  5562. // - e.g. retrieved from "ModelRoot/TableName/TableID/BlobFieldName"
  5563. URIBlobFieldName: RawUTF8;
  5564. /// position of the &session_signature=... text in Call^.url string
  5565. URISessionSignaturePos: integer;
  5566. /// the Table as specified at the URI level (if any)
  5567. Table: TSQLRecordClass;
  5568. /// the index in the Model of the Table specified at the URI level (if any)
  5569. TableIndex: integer;
  5570. /// the RTTI properties of the Table specified at the URI level (if any)
  5571. TableRecordProps: TSQLModelRecordProperties;
  5572. /// the RESTful instance implementing the Table specified at the URI level (if any)
  5573. // - equals TSQLRestServer most of the time, but may be an TSQLRestStorage
  5574. // for any in-memory/MongoDB/virtual instance
  5575. TableEngine: TSQLRest;
  5576. /// the associated TSQLRecord.ID, as decoded from URI scheme
  5577. // - this property will be set from incoming URI, even if RESTful
  5578. // authentication is not enabled
  5579. TableID: TID;
  5580. /// the current execution command
  5581. Command: TSQLRestServerURIContextCommand;
  5582. /// the index of the callback published method within the internal class list
  5583. MethodIndex: integer;
  5584. /// the service identified by an interface-based URI
  5585. Service: TServiceFactoryServer;
  5586. /// the method index for an interface-based service
  5587. // - Service member has already be retrieved from URI (so is not nil)
  5588. // - 0..2 are the internal _free_/_contract_/_signature_ pseudo-methods
  5589. ServiceMethodIndex: integer;
  5590. /// the JSON array of parameters for an the interface-based service
  5591. // - Service member has already be retrieved from URI (so is not nil)
  5592. ServiceParameters: PUTF8Char;
  5593. /// the instance ID for interface-based services instance
  5594. // - can be e.g. the client session ID for sicPerSession or the thread ID for
  5595. // sicPerThread
  5596. ServiceInstanceID: PtrUInt;
  5597. /// the current execution context of an interface-based service
  5598. // - maps to Service.fExecution[ServiceMethodIndex]
  5599. ServiceExecution: PServiceFactoryExecution;
  5600. /// force the interface-based service methods to return a JSON object
  5601. // - default behavior is to follow Service.ResultAsJSONObject property value
  5602. // (which own default is to return a more convenient JSON array)
  5603. // - if set to TRUE, this execution context will FORCE the method to return
  5604. // a JSON object, even if Service.ResultAsJSONObject=false: this may be
  5605. // handy when the method is executed from a JavaScript content
  5606. ForceServiceResultAsJSONObject: boolean;
  5607. /// force the interface-based service methods to return a plain JSON object
  5608. // - i.e. '{....}' instead of '{"result":{....}}'
  5609. // - only set if ForceServiceResultAsJSONObject=TRUE and if no ID is about
  5610. // to be returned
  5611. // - could be used e.g. for stateless interaction with a (non mORMot)
  5612. // stateless JSON REST Server
  5613. ForceServiceResultAsJSONObjectWithoutResult: boolean;
  5614. /// force the interface-based service methods to return a XML object
  5615. // - default behavior is to follow Service.ResultAsJSONObject property value
  5616. // (which own default is to return a more convenient JSON array)
  5617. // - if set to TRUE, this execution context will FORCE the method to return
  5618. // a XML object, by setting ForceServiceResultAsJSONObject then converting
  5619. // the resulting JSON object into the corresponding XML via JSONBufferToXML()
  5620. // - TSQLRestServerURIContext.InternalExecuteSOAByInterface will inspect the
  5621. // Accept HTTP header to check if the answer should be XML rather than JSON
  5622. ForceServiceResultAsXMLObject: boolean;
  5623. /// specify a custom name space content when returning a XML object
  5624. // - default behavior is to follow Service.ResultAsXMLObjectNameSpace
  5625. // property (which is void by default)
  5626. // - service may set e.g. XMLUTF8_NAMESPACE, which will append <content ...>
  5627. // </content> around the generated XML data, to avoid validation problems
  5628. // or set a particular XML name space, depending on the application
  5629. ForceServiceResultAsXMLObjectNameSpace: RawUTF8;
  5630. /// URI inlined parameters
  5631. // - use UrlDecodeValue*() functions to retrieve the values
  5632. // - for mPOST requests, would also be filled for following content types:
  5633. // ! application/x-www-form-urlencoded or multipart/form-data
  5634. Parameters: PUTF8Char;
  5635. /// URI inlined parameters position in Call^.url string
  5636. // - use Parameters field to retrieve the values
  5637. ParametersPos: integer;
  5638. /// access to all input/output parameters at TSQLRestServer.URI() level
  5639. // - process should better call Results() or Success() methods to set the
  5640. // appropriate answer or Error() method in case of an error
  5641. // - low-level access to the call parameters can be made via this pointer
  5642. Call: PSQLRestURIParams;
  5643. /// the corresponding session TAuthSession.IDCardinal value
  5644. // - equals 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED) if the session
  5645. // is not started yet - i.e. if still in handshaking phase
  5646. // - equals 1 (CONST_AUTHENTICATION_NOT_USED) if authentication mode
  5647. // is not enabled - i.e. if TSQLRestServer.HandleAuthentication = FALSE
  5648. Session: cardinal;
  5649. /// the corresponding TAuthSession.User.GroupRights.ID value
  5650. // - is undefined if Session is 0 or 1 (no authentication running)
  5651. SessionGroup: integer;
  5652. /// the corresponding TAuthSession.User.ID value
  5653. // - is undefined if Session is 0 or 1 (no authentication running)
  5654. SessionUser: TID;
  5655. /// the corresponding TAuthSession.User.LogonName value
  5656. // - is undefined if Session is 0 or 1 (no authentication running)
  5657. SessionUserName: RawUTF8;
  5658. /// the remote IP from which the TAuthSession was created, if any
  5659. // - is undefined if Session is 0 or 1 (no authentication running)
  5660. SessionRemoteIP: RawUTF8;
  5661. /// the static instance corresponding to the associated Table (if any)
  5662. {$ifdef FPC}&Static{$else}Static{$endif}: TSQLRest;
  5663. /// the kind of static instance corresponding to the associated Table (if any)
  5664. StaticKind: TSQLRestServerKind;
  5665. /// optional error message which will be transmitted as JSON error (if set)
  5666. CustomErrorMsg: RawUTF8;
  5667. /// high-resolution timimg of the execution command, in micro-seconds
  5668. // - only set when TSQLRestServer.URI finished
  5669. MicroSecondsElapsed: QWord;
  5670. {$ifdef WITHLOG}
  5671. /// associated logging instance for the current thread on the server
  5672. // - you can use it to log some process on the server side
  5673. Log: TSynLog;
  5674. {$endif}
  5675. /// finalize the execution context
  5676. destructor Destroy; override;
  5677. /// extract the input parameters from its URI
  5678. // - you should not have to call this method directly, but rather
  5679. // all the InputInt/InputDouble/InputUTF8/InputExists/... properties
  5680. // - may be useful if you want to access directly to InputPairs[] with no
  5681. // prior knowledge of the input parameter names
  5682. // - you can specify a title text to optionally log the input array
  5683. procedure FillInput(const LogInputIdent: RawUTF8='');
  5684. /// retrieve one input parameter from its URI name as Int64
  5685. // - raise an EParsingException if the parameter is not found
  5686. property InputInt[const ParamName: RawUTF8]: Int64 read GetInputInt;
  5687. /// retrieve one input parameter from its URI name as double
  5688. // - raise an EParsingException if the parameter is not found
  5689. property InputDouble[const ParamName: RawUTF8]: double read GetInputDouble;
  5690. /// retrieve one input parameter from its URI name as RawUTF8
  5691. // - raise an EParsingException if the parameter is not found
  5692. property InputUTF8[const ParamName: RawUTF8]: RawUTF8 read GetInputUTF8;
  5693. /// retrieve one input parameter from its URI name as a VCL string
  5694. // - raise an EParsingException if the parameter is not found
  5695. // - prior to Delphi 2009, some Unicode characters may be missing in the
  5696. // returned AnsiString value
  5697. property InputString[const ParamName: RawUTF8]: string read GetInputString;
  5698. /// retrieve one input parameter from its URI name as Int64
  5699. // - returns 0 if the parameter is not found
  5700. property InputIntOrVoid[const ParamName: RawUTF8]: Int64 read GetInputIntOrVoid;
  5701. /// retrieve one hexadecimal input parameter from its URI name as cardinal
  5702. // - returns 0 if the parameter is not found
  5703. property InputHexaOrVoid[const ParamName: RawUTF8]: cardinal read GetInputHexaOrVoid;
  5704. /// retrieve one input parameter from its URI name as double
  5705. // - returns 0 if the parameter is not found
  5706. property InputDoubleOrVoid[const ParamName: RawUTF8]: double read GetInputDoubleOrVoid;
  5707. /// retrieve one input parameter from its URI name as RawUTF8
  5708. // - returns '' if the parameter is not found
  5709. property InputUTF8OrVoid[const ParamName: RawUTF8]: RawUTF8 read GetInputUTF8OrVoid;
  5710. /// retrieve one input parameter from its URI name as a VCL string
  5711. // - returns '' if the parameter is not found
  5712. // - prior to Delphi 2009, some Unicode characters may be missing in the
  5713. // returned AnsiString value
  5714. property InputStringOrVoid[const ParamName: RawUTF8]: string read GetInputStringOrVoid;
  5715. /// retrieve one input parameter from its URI name as RawUTF8
  5716. // - returns FALSE and call Error(ErrorMessageForMissingParameter) - which
  5717. // may be a resourcestring - if the parameter is not found
  5718. // - returns TRUE and set Value if the parameter is found
  5719. function InputUTF8OrError(const ParamName: RawUTF8; out Value: RawUTF8;
  5720. const ErrorMessageForMissingParameter: string): boolean;
  5721. /// retrieve one input parameter from its URI name as RawUTF8
  5722. // - returns supplied DefaultValue if the parameter is not found
  5723. function InputUTF8OrDefault(const ParamName, DefaultValue: RawUTF8): RawUTF8;
  5724. /// retrieve one input parameter from its URI name as an enumeration
  5725. // - will expect the value to be specified as integer, or as the textual
  5726. // representation of the enumerate, ignoring any optional lowercase prefix
  5727. // as featured by TEnumType.GetEnumNameValue()
  5728. // - returns TRUE and set ValueEnum if the parameter is found and correct
  5729. // - returns FALSE and set ValueEnum to first item (i.e. DefaultEnumOrd) if
  5730. // the parameter is not found, or not containing a correct value
  5731. function InputEnum(const ParamName: RawUTF8; EnumType: PTypeInfo;
  5732. out ValueEnum; DefaultEnumOrd: integer=0): boolean;
  5733. /// return TRUE if the input parameter is available at URI
  5734. // - even if InputUTF8['param']='', there may be '..?param=&another=2'
  5735. property InputExists[const ParamName: RawUTF8]: Boolean read GetInputExists;
  5736. {$ifndef NOVARIANTS}
  5737. /// retrieve one input parameter from its URI name as variant
  5738. // - if the parameter value is text, it is stored in the variant as
  5739. // a generic VCL string content: so before Delphi 2009, you may loose
  5740. // some characters at decoding from UTF-8 input buffer
  5741. // - raise an EParsingException if the parameter is not found
  5742. property Input[const ParamName: RawUTF8]: variant read GetInput; default;
  5743. /// retrieve one input parameter from its URI name as variant
  5744. // - if the parameter value is text, it is stored in the variant as
  5745. // a RawUTF8: so before Delphi 2009, you won't loose any Unicode character,
  5746. // but you should convert its value to AnsiString using UTF8ToString()
  5747. // - returns Unassigned if the parameter is not found
  5748. property InputOrVoid[const ParamName: RawUTF8]: variant read GetInputOrVoid;
  5749. /// retrieve one input parameter from its URI name as variant
  5750. // - returns FALSE and call Error(ErrorMessageForMissingParameter) - which
  5751. // may be a resourcestring - if the parameter is not found
  5752. // - returns TRUE and set Value if the parameter is found
  5753. // - if the parameter value is text, it is stored in the variant as
  5754. // a RawUTF8: so before Delphi 2009, you won't loose any Unicode character,
  5755. // but you should convert its value to AnsiString using UTF8ToString()
  5756. function InputOrError(const ParamName: RawUTF8; out Value: variant;
  5757. const ErrorMessageForMissingParameter: string): boolean;
  5758. /// retrieve all input parameters from URI as a variant JSON object
  5759. // - returns Unassigned if no parameter was defined
  5760. // - returns a JSON object with input parameters encoded as
  5761. // ! {"name1":value1,"name2":value2...}
  5762. // - if the parameters were encoded as multipart, the JSON object
  5763. // will be encoded with its textual values, or with nested objects, if
  5764. // the data was supplied as binary:
  5765. // ! {"name1":{"data":..,"filename":...,"contenttype":...},"name2":...}
  5766. // since name1.data would be Base64 encoded, so you should better
  5767. // use the InputAsMultiPart() method instead when working with binary
  5768. property InputAsTDocVariant: variant read GetInputAsTDocVariant;
  5769. {$endif}
  5770. /// decode any multipart/form-data POST request input
  5771. // - returns TRUE and set MultiPart array as expected, on success
  5772. function InputAsMultiPart(var MultiPart: TMultiPartDynArray): Boolean;
  5773. /// low-level access to the input parameters, stored as pairs of UTF-8
  5774. // - even items are parameter names, odd are values
  5775. // - Input*[] properties should have been called previously to fill the
  5776. // internal array, or by calling FillInput if you do not know the input
  5777. // parameters which may appear
  5778. property InputPairs: TRawUTF8DynArray read FInput;
  5779. /// retrieve an incoming HTTP header
  5780. // - the supplied header name is case-insensitive
  5781. // - you could call e.g. InHeader['remoteip'] to retrieve the caller IP
  5782. property InHeader[const HeaderName: RawUTF8]: RawUTF8 read GetInHeader;
  5783. /// retrieve an incoming HTTP cookie value
  5784. // - the supplied cookie name is case-insensitive
  5785. property InCookie[CookieName: RawUTF8]: RawUTF8 read GetInCookie write SetInCookie;
  5786. /// define a new 'name=value' cookie to be returned to the client
  5787. // - if not void, TSQLRestServer.URI() will define a new 'set-cookie: ...'
  5788. // header in Call^.OutHead
  5789. // - you can use COOKIE_EXPIRED as value to delete a cookie in the browser
  5790. // - if no Path=/.. is included, it will append '; Path=/'+Server.Model.Root
  5791. property OutSetCookie: RawUTF8 read fOutSetCookie write SetOutSetCookie;
  5792. /// retrieve the "User-Agent" value from the incoming HTTP headers
  5793. property UserAgent: RawUTF8 read GetUserAgent;
  5794. /// identify which kind of client is actually connected
  5795. // - the "User-Agent" HTTP will be checked for 'mORMot' substring, and
  5796. // set ckFramework on match
  5797. // - either ckAjax for a classic (AJAX) browser, or any other kind of
  5798. // HTTP client
  5799. // - will be used e.g. by ClientSQLRecordOptions to check if the
  5800. // current remote client expects standard JSON in all cases
  5801. function ClientKind: TSQLRestServerURIContextClientKind;
  5802. /// identify if the request is about a Table containing nested objects or
  5803. // arrays, which could be serialized as JSON objects or arrays, instead
  5804. // of plain JSON string (as stored in the database)
  5805. // - will idenfity ClientKind=ckAjax, or check for rsoGetAsJsonNotAsString
  5806. // in TSQLRestServer.Options
  5807. function ClientSQLRecordOptions: TJSONSerializerSQLRecordOptions;
  5808. /// true if called from TSQLRestServer.AdministrationExecute
  5809. function IsRemoteAdministrationExecute: boolean;
  5810. /// compute the file name corresponding to the URI
  5811. // - e.g. '/root/methodname/toto/index.html' will return 'toto\index.html'
  5812. property ResourceFileName: TFileName read GetResourceFileName;
  5813. /// use this method to send back directly a result value to the caller
  5814. // - expects Status to be either HTML_SUCCESS, HTML_NOTMODIFIED,
  5815. // HTML_CREATED, or HTML_TEMPORARYREDIRECT, and will return as answer the
  5816. // supplied Result content with no transformation
  5817. // - if Status is an error code, it will call Error() method
  5818. // - CustomHeader optional parameter can be set e.g. to
  5819. // TEXT_CONTENT_TYPE_HEADER if the default JSON_CONTENT_TYPE is not OK,
  5820. // or calling GetMimeContentTypeHeader() on the returned binary buffer
  5821. // - if Handle304NotModified is TRUE and Status is HTML_SUCCESS, the Result
  5822. // content will be hashed (using crc32c) and in case of no modification
  5823. // will return HTML_NOTMODIFIED to the browser, without the actual result
  5824. // content (to save bandwidth)
  5825. procedure Returns(const Result: RawUTF8; Status: integer=HTML_SUCCESS;
  5826. const CustomHeader: RawUTF8=''; Handle304NotModified: boolean=false;
  5827. HandleErrorAsRegularResult: boolean=false); overload;
  5828. /// use this method to send back a JSON object to the caller
  5829. // - this method will encode the supplied values e.g. as
  5830. // ! JSONEncode(['name','John','year',1972]) = '{"name":"John","year":1972}'
  5831. // - implementation is just a wrapper around Returns(JSONEncode([]))
  5832. // - note that cardinal values should be type-casted to Int64() (otherwise
  5833. // the integer mapped value will be transmitted, therefore wrongly)
  5834. // - expects Status to be either HTML_SUCCESS or HTML_CREATED
  5835. // - caller can set Handle304NotModified=TRUE for Status=HTML_SUCCESS
  5836. procedure Returns(const NameValuePairs: array of const; Status: integer=HTML_SUCCESS;
  5837. Handle304NotModified: boolean=false; HandleErrorAsRegularResult: boolean=false); overload;
  5838. /// use this method to send back any object as JSON document to the caller
  5839. // - this method will call ObjectToJson() to compute the returned content
  5840. // - you can customize SQLRecordOptions, to force the returned JSON
  5841. // object to have its TSQLRecord nested fields serialized as true JSON
  5842. // arrays or objects, or add an "ID_str" string field for JavaScript
  5843. procedure Returns(Value: TObject; Status: integer=HTML_SUCCESS;
  5844. Handle304NotModified: boolean=false;
  5845. SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]); overload;
  5846. /// use this method to send back any variant as JSON to the caller
  5847. // - this method will call VariantSaveJSON() to compute the returned content
  5848. procedure ReturnsJson(const Value: variant; Status: integer=HTML_SUCCESS;
  5849. Handle304NotModified: boolean=false; Escape: TTextWriterKind=twJSONEscape;
  5850. MakeHumanReadable: boolean=false);
  5851. /// uses this method to send back directly any binary content to the caller
  5852. // - the exact MIME type will be retrieved using GetMimeContentTypeHeader(),
  5853. // from the supplied Blob binary buffer, and optional a file name
  5854. // - by default, the HTML_NOTMODIFIED process will take place, to minimize
  5855. // bandwidth between the server and the client
  5856. procedure ReturnBlob(const Blob: RawByteString; Status: integer=HTML_SUCCESS;
  5857. Handle304NotModified: boolean=true; const FileName: TFileName='');
  5858. /// use this method to send back a file to the caller
  5859. // - this method will let the HTTP server return the file content
  5860. // - if Handle304NotModified is TRUE, will check the file age to ensure
  5861. // that the file content will be sent back to the server only if it changed
  5862. // - if ContentType is left to default '', method will guess the expected
  5863. // mime-type from the file name extension
  5864. // - if the file name does not exist, a generic 404 error page would be
  5865. // returned, unless an explicit redirection is defined in Error404Redirect
  5866. // - you can also specify the resulting file name, as downloaded and written
  5867. // by the client browser, in the optional AttachmentFileName parameter, if
  5868. // the URI does not match the expected file name
  5869. procedure ReturnFile(const FileName: TFileName;
  5870. Handle304NotModified: boolean=false; const ContentType: RawUTF8='';
  5871. const AttachmentFileName: RawUTF8=''; const Error404Redirect: RawUTF8='');
  5872. /// use this method to send back a file from a local folder to the caller
  5873. // - URIBlobFieldName value, as parsed from the URI, would containn the
  5874. // expected file name in the local folder, using DefaultFileName if the
  5875. // URI is void, and redirecting to Error404Redirect if the file is not found
  5876. // - this method will let the HTTP server return the file content
  5877. // - if Handle304NotModified is TRUE, will check the file age to ensure
  5878. // that the file content will be sent back to the server only if it changed
  5879. procedure ReturnFileFromFolder(const FolderName: TFileName;
  5880. Handle304NotModified: boolean=true; const DefaultFileName: TFileName='index.html';
  5881. const Error404Redirect: RawUTF8='');
  5882. /// use this method notify the caller that the resource URI has changed
  5883. // - returns a HTML_TEMPORARYREDIRECT status with the specified location,
  5884. // or HTML_MOVEDPERMANENTLY if PermanentChange is TRUE
  5885. procedure Redirect(const NewLocation: RawUTF8; PermanentChange: boolean=false);
  5886. /// use this method to send back a JSON object with a "result" field
  5887. // - this method will encode the supplied values as a {"result":"...}
  5888. // JSON object, as such for one value:
  5889. // $ {"result":"OneValue"}
  5890. // (with one value, you can just call TSQLRestClientURI.CallBackGetResult
  5891. // method to call and decode this value)
  5892. // or as a JSON object containing an array of values:
  5893. // $ {"result":["One","two"]}
  5894. // - expects Status to be either HTML_SUCCESS or HTML_CREATED
  5895. // - caller can set Handle304NotModified=TRUE for Status=HTML_SUCCESS
  5896. procedure Results(const Values: array of const; Status: integer=HTML_SUCCESS;
  5897. Handle304NotModified: boolean=false);
  5898. /// use this method if the caller expect no data, just a status
  5899. // - just wrap the overloaded Returns() method with no result value
  5900. // - if Status is an error code, it will call Error() method
  5901. // - by default, calling this method will mark process as successfull
  5902. procedure Success(Status: integer=HTML_SUCCESS); virtual;
  5903. /// use this method to send back an error to the caller
  5904. // - expects Status to not be HTML_SUCCESS neither HTML_CREATED,
  5905. // and will send back a JSON error message to the caller, with the
  5906. // supplied error text
  5907. // - if no ErrorMessage is specified, will return a default text
  5908. // corresponding to the Status code
  5909. procedure Error(const ErrorMessage: RawUTF8='';
  5910. Status: integer=HTML_BADREQUEST); overload; virtual;
  5911. /// use this method to send back an error to the caller
  5912. // - implementation is just a wrapper over Error(FormatUTF8(Format,Args))
  5913. procedure Error(const Format: RawUTF8; const Args: array of const;
  5914. Status: integer=HTML_BADREQUEST); overload;
  5915. /// use this method to send back an error to the caller
  5916. // - will serialize the supplied exception, with an optional error message
  5917. procedure Error(E: Exception; const Format: RawUTF8; const Args: array of const;
  5918. Status: integer=HTML_BADREQUEST); overload;
  5919. /// implements a method-based service for live update of some settings
  5920. // - should be called from a method-based service, e.g. Configuration()
  5921. // - the settings are expected to be stored e.g. in a TSynAutoCreateFields
  5922. // instance, potentially with nested objects
  5923. // - accept the following REST methods to read and write the settings:
  5924. // ! GET http://server:888/root/configuration
  5925. // ! GET http://server:888/root/configuration/propname
  5926. // ! GET http://server:888/root/configuration/propname?value=propvalue
  5927. // - could be used e.g. as such:
  5928. // ! procedure TMyRestServerMethods.Configuration(Ctxt: TSQLRestServerURIContext);
  5929. // ! begin // http://server:888/myrestserver/configuration/name?value=newValue
  5930. // ! Ctxt.ConfigurationRestMethod(fSettings);
  5931. // ! end;
  5932. procedure ConfigurationRestMethod(SettingsStorage: TObject);
  5933. /// at Client Side, compute URI and BODY according to the routing scheme
  5934. // - abstract implementation which is to be overridden
  5935. // - as input, method should be the method name to be executed,
  5936. // params should contain the incoming parameters as JSON CSV (without []),
  5937. // and clientDriven ID should contain the optional Client ID value
  5938. // - at output, should update the HTTP uri corresponding to the proper
  5939. // routing, and should return the corresponding HTTP body within sent
  5940. class procedure ClientSideInvoke(var uri: RawUTF8;
  5941. const method, params, clientDrivenID: RawUTF8; out sent: RawUTF8); virtual; abstract;
  5942. end;
  5943. /// calling context for a TSQLRestServerCallBack using simple REST for
  5944. // interface-based services
  5945. // - this class will use RESTful routing for interface-based services:
  5946. // method name will be identified within the URI, as
  5947. // $ /Model/Interface.Method[/ClientDrivenID]
  5948. // e.g. for ICalculator.Add:
  5949. // $ POST /root/Calculator.Add
  5950. // $ (...)
  5951. // $ [1,2]
  5952. // or, for a sicClientDriven mode service:
  5953. // $ POST /root/ComplexNumber.Add/1234
  5954. // $ (...)
  5955. // $ [20,30]
  5956. // in this case, the sent content will be a JSON array of [parameters...]
  5957. // - as an alternative, input parameters may be encoded at URI level (with
  5958. // a size limit depending on the HTTP routers, whereas there is no such
  5959. // limitation when they are transmitted as message body)
  5960. // - one benefit of having .../ClientDrivenID encoded at URI is that it will
  5961. // be more secured in our RESTful authentication scheme: each method and even
  5962. // client driven session will be signed individualy
  5963. TSQLRestRoutingREST = class(TSQLRestServerURIContext)
  5964. protected
  5965. /// retrieve interface-based SOA with URI RESTful routing
  5966. // - should set Service member (and possibly ServiceMethodIndex)
  5967. // - this overridden implementation expects an URI encoded with
  5968. // '/Model/Interface.Method[/ClientDrivenID]' for this class, and
  5969. // will set ServiceMethodIndex for next ExecuteSOAByInterface method call
  5970. procedure URIDecodeSOAByInterface; override;
  5971. /// direct launch of an interface-based service with URI RESTful routing
  5972. // - this overridden implementation expects parameters to be sent as one JSON
  5973. // array body (Delphi/AJAX way) or optionally with URI decoding (HTML way):
  5974. // ! function TServiceCalculator.Add(n1, n2: integer): integer;
  5975. // would accept such requests:
  5976. // ! URL='root/Calculator.Add' and InBody='[ 1,2 ]'
  5977. // ! URL='root/Calculator.Add?+%5B+1%2C2+%5D' // decoded as ' [ 1,2 ]'
  5978. // ! URL='root/Calculator.Add?n1=1&n2=2' // in any order, even missing
  5979. procedure ExecuteSOAByInterface; override;
  5980. public
  5981. /// at Client Side, compute URI and BODY according to RESTful routing scheme
  5982. // - e.g. on input uri='root/Calculator', method='Add', params='1,2' and
  5983. // clientDrivenID='1234' -> on output uri='root/Calculator.Add/1234' and
  5984. // sent='[1,2]'
  5985. class procedure ClientSideInvoke(var uri: RawUTF8;
  5986. const method, params, clientDrivenID: RawUTF8; out sent: RawUTF8); override;
  5987. end;
  5988. /// calling context for a TSQLRestServerCallBack using JSON/RPC for
  5989. // interface-based services
  5990. // - in this routing scheme, the URI will define the interface, then the
  5991. // method name will be inlined with parameters, e.g.
  5992. // $ POST /root/Calculator
  5993. // $ (...)
  5994. // $ {"method":"Add","params":[1,2]}
  5995. // or, for a sicClientDriven mode service:
  5996. // $ POST /root/ComplexNumber
  5997. // $ (...)
  5998. // $ {"method":"Add","params":[20,30],"id":1234}
  5999. TSQLRestRoutingJSON_RPC = class(TSQLRestServerURIContext)
  6000. protected
  6001. /// retrieve interface-based SOA with URI JSON/RPC routing
  6002. // - this overridden implementation expects an URI encoded with
  6003. // '/Model/Interface' as for the JSON/RPC routing scheme, and won't
  6004. // set ServiceMethodIndex at this level (but in ExecuteSOAByInterface)
  6005. procedure URIDecodeSOAByInterface; override;
  6006. /// direct launch of an interface-based service with URI JSON/RPC routing
  6007. // - URI() will ensure that Service<>nil before calling it
  6008. // - this overridden implementation expects parameters to be sent as part
  6009. // of a JSON object body:
  6010. // $ {"method":"Add","params":[20,30],"id":1234}
  6011. procedure ExecuteSOAByInterface; override;
  6012. public
  6013. /// at Client Side, compute URI and BODY according to JSON/RPC routing scheme
  6014. // - e.g. on input uri='root/Calculator', method='Add', params='1,2' and
  6015. // clientDrivenID='1234' -> on output uri='root/Calculator' and
  6016. // sent={"method":"Add","params":[1,2],"id":1234}
  6017. class procedure ClientSideInvoke(var uri: RawUTF8;
  6018. const method, params, clientDrivenID: RawUTF8; out sent: RawUTF8); override;
  6019. end;
  6020. /// method prototype to be used on Server-Side for method-based services
  6021. // - will be routed as ModelRoot/[TableName/TableID/]MethodName RESTful requests
  6022. // - this mechanism is able to handle some custom Client/Server request, similar
  6023. // to the DataSnap technology, but in a KISS way; it's fully integrated in the
  6024. // Client/Server architecture of our framework
  6025. // - just add a published method of this type to any TSQLRestServer descendant
  6026. // - when TSQLRestServer.URI receive a request for ModelRoot/MethodName
  6027. // or ModelRoot/TableName/TableID/MethodName, it will check for a published method
  6028. // in its self instance named MethodName which MUST be of TSQLRestServerCallBack
  6029. // type (not checked neither at compile time neither at runtime: beware!) and
  6030. // call it to handle the request
  6031. // - important warning: the method implementation MUST be thread-safe
  6032. // - when TSQLRestServer.URI receive a request for ModelRoot/MethodName,
  6033. // it calls the corresponding published method with aRecord set to nil
  6034. // - when TSQLRestServer.URI receive a request for ModelRoot/TableName/TableID/MethodName,
  6035. // it calls the corresponding published method with aRecord pointing to a
  6036. // just created instance of the corresponding class, with its field ID set;
  6037. // note that the only set field is ID: other fields of aRecord are not set, but
  6038. // must secificaly be retrieved on purpose
  6039. // - for ModelRoot/TableName/TableID/MethodName, the just created instance will
  6040. // be freed by TSQLRestServer.URI when the method returns
  6041. // - Ctxt.Parameters values are set from incoming URI, and each parameter can be
  6042. // retrieved with a loop like this:
  6043. // ! if not UrlDecodeNeedParameters(Ctxt.Parameters,'SORT,COUNT') then
  6044. // ! exit;
  6045. // ! while Ctxt.Parameters<>nil do begin
  6046. // ! UrlDecodeValue(Ctxt.Parameters,'SORT=',aSortString);
  6047. // ! UrlDecodeValueInteger(Ctxt.Parameters,'COUNT=',aCountInteger,@Ctxt.Parameters);
  6048. // ! end;
  6049. // - Ctxt.Call is set with low-level incoming and outgoing data from client
  6050. // (e.g. Ctxt.Call.InBody contain POST/PUT data message)
  6051. // - Ctxt.Session* will identify to the authentication session of the remote client
  6052. // (CONST_AUTHENTICATION_NOT_USED=1 if authentication mode is not enabled or
  6053. // CONST_AUTHENTICATION_SESSION_NOT_STARTED=0 if the session not started yet) -
  6054. // code may use SessionGetUser() method to retrieve the user details
  6055. // - Ctxt.Method will indicate the used HTTP verb (e.g. GET/POST/PUT..)
  6056. // - if process succeeded, implementation shall call Ctxt.Results([]) method to
  6057. // set a JSON response object with one "result" field name or Ctxt.Returns([])
  6058. // with a JSON object described in Name/Value pairs; if the returned value is
  6059. // not JSON_CONTENT_TYPE, use Ctxt.Returns() and its optional CustomHeader
  6060. // parameter can specify a custom header like TEXT_CONTENT_TYPE_HEADER
  6061. // - if process succeeded, and no data is expected to be returned to the caller,
  6062. // implementation shall call overloaded Ctxt.Success() method with the
  6063. // expected status (i.e. just Ctxt.Success will return HTML_SUCCESS)
  6064. // - if process failed, implementation shall call Ctxt.Error() method to
  6065. // set the corresponding error message and error code number
  6066. // - a typical implementation may be:
  6067. // ! procedure TSQLRestServerTest.Sum(Ctxt: TSQLRestServerURIContext);
  6068. // ! var a,b: TSynExtended;
  6069. // ! begin
  6070. // ! if UrlDecodeNeedParameters(Ctxt.Parameters,'A,B') then begin
  6071. // ! while Ctxt.Parameters<>nil do begin
  6072. // ! UrlDecodeExtended(Ctxt.Parameters,'A=',a);
  6073. // ! UrlDecodeExtended(Ctxt.Parameters,'B=',b,@Ctxt.Parameters);
  6074. // ! end;
  6075. // ! Ctxt.Results([a+b]);
  6076. // ! // same as: Ctxt.Returns(JSONEncode(['result',a+b]));
  6077. // ! // same as: Ctxt.Returns(['result',a+b]);
  6078. // ! end else
  6079. // ! Ctxt.Error('Missing Parameter');
  6080. // ! end;
  6081. // - Client-Side can be implemented as you wish. By convention, it could be
  6082. // appropriate to define in either TSQLRestServer (if to be called as
  6083. // ModelRoot/MethodName), either TSQLRecord (if to be called as
  6084. // ModelRoot/TableName[/TableID]/MethodName) a custom public or protected method,
  6085. // calling TSQLRestClientURI.URL with the appropriate parameters, and named
  6086. // (by convention) as MethodName; TSQLRestClientURI has dedicated methods
  6087. // like CallBackGetResult, CallBackGet, CallBackPut and CallBack; see also
  6088. // TSQLModel.getURICallBack and JSONDecode function
  6089. // ! function TSQLRecordPeople.Sum(aClient: TSQLRestClientURI; a, b: double): double;
  6090. // ! var err: integer;
  6091. // ! begin
  6092. // ! val(aClient.CallBackGetResult('sum',['a',a,'b',b]),result,err);
  6093. // ! end;
  6094. TSQLRestServerCallBack = procedure(Ctxt: TSQLRestServerURIContext) of object;
  6095. /// description of a method-based service
  6096. TSQLRestServerMethod = record
  6097. /// the method name
  6098. Name: RawUTF8;
  6099. /// the event which will be executed for this method
  6100. CallBack: TSQLRestServerCallBack;
  6101. /// set to TRUE disable Authentication check for this method
  6102. // - use TSQLRestServer.ServiceMethodByPassAuthentication() method
  6103. ByPassAuthentication: boolean;
  6104. /// detailed statistics associated with this method
  6105. Stats: TSynMonitorInputOutput;
  6106. end;
  6107. /// used to store all method-based services of a TSQLRestServer instance
  6108. TSQLRestServerMethods = array of TSQLRestServerMethod;
  6109. /// pointer to a description of a method-based service
  6110. PSQLRestServerMethod = ^TSQLRestServerMethod;
  6111. /// the possible options for handling table names
  6112. TSQLCheckTableName = (ctnNoCheck,ctnMustExist,ctnTrimExisting);
  6113. /// the possible options for TSQLRestServer.CreateMissingTables and
  6114. // TSQLRecord.InitializeTable methods
  6115. // - itoNoAutoCreateGroups and itoNoAutoCreateUsers will avoid
  6116. // TSQLAuthGroup.InitializeTable to fill the TSQLAuthGroup and TSQLAuthUser
  6117. // tables with default records
  6118. // - itoNoCreateMissingField will avoid to create the missing fields on a table
  6119. // - itoNoIndex4ID won't create the index for the main ID field
  6120. // - itoNoIndex4UniqueField won't create indexes for "stored AS_UNIQUE" fields
  6121. // - itoNoIndex4NestedRecord won't create indexes for TSQLRecord fields
  6122. // - itoNoIndex4RecordReference won't create indexes for TRecordReference fields
  6123. // - itoNoIndex4TID won't create indexes for TID fields
  6124. // - itoNoIndex4RecordVersion won't create indexes for TRecordVersion fields
  6125. // - INITIALIZETABLE_NOINDEX constant contain all itoNoIndex* items
  6126. TSQLInitializeTableOption = (
  6127. itoNoAutoCreateGroups, itoNoAutoCreateUsers,
  6128. itoNoCreateMissingField,
  6129. itoNoIndex4ID, itoNoIndex4UniqueField,
  6130. itoNoIndex4NestedRecord, itoNoIndex4RecordReference,
  6131. itoNoIndex4TID, itoNoIndex4RecordVersion);
  6132. /// the options to be specified for TSQLRestServer.CreateMissingTables and
  6133. // TSQLRecord.InitializeTable methods
  6134. TSQLInitializeTableOptions = set of TSQLInitializeTableOption;
  6135. /// a dynamic array of TSQLRecordMany instances
  6136. TSQLRecordManyObjArray = array of TSQLRecordMany;
  6137. /// internal data used by TSQLRecord.FillPrepare()/FillPrepareMany() methods
  6138. // - using a dedicated class will reduce memory usage for each TSQLRecord
  6139. // instance (which won't need these properties most of the time)
  6140. TSQLRecordFill = class
  6141. protected
  6142. /// associated table
  6143. fTable: TSQLTable;
  6144. /// current retrieved row
  6145. fFillCurrentRow: integer;
  6146. /// number of used items in TableMap[] array
  6147. // - calculated in FillPrepare() or FillPrepareMany() methods
  6148. fTableMapCount: integer;
  6149. /// set by TSQLRecord.FillPrepareMany() to release M.fDestID^ instances
  6150. fTableMapRecordManyInstances: TSQLRecordManyObjArray;
  6151. /// map the published fields index
  6152. // - calculated in FillPrepare() or FillPrepareMany() methods
  6153. fTableMap: array of record
  6154. /// the class instance to be filled from the TSQLTable
  6155. // - can be a TSQLRecordMany instance after FillPrepareMany() method call
  6156. Dest: TSQLRecord;
  6157. /// the published property RTTI to be filled from the TSQLTable
  6158. // - is nil for the RowID/ID field
  6159. DestField: TSQLPropInfo;
  6160. /// the column index in TSQLTable
  6161. TableIndex: integer;
  6162. end;
  6163. /// mark all mapped or TModTime fields
  6164. fTableMapFields: TSQLFieldBits;
  6165. /// if Joined instances were initialized via TSQLRecord.CreateJoined()
  6166. fJoinedFields: boolean;
  6167. /// return fJoinedFields or false if self=nil
  6168. function GetJoinedFields: boolean;
  6169. {$ifdef HASINLINE}inline;{$endif}
  6170. /// add a property to the fTableMap[] array
  6171. // - aIndex is the column index in TSQLTable
  6172. procedure AddMap(aRecord: TSQLRecord; aField: TSQLPropInfo; aIndex: integer); overload;
  6173. /// add a property to the fTableMap[] array
  6174. // - aIndex is the column index in TSQLTable
  6175. procedure AddMap(aRecord: TSQLRecord; const aFieldName: RawUTF8; aIndex: integer); overload;
  6176. /// add all simple property names, with to the fTableMap[] array
  6177. // - will map ID/RowID, then all simple fields of this TSQLRecord
  6178. // - aIndex is the column index in TSQLTable
  6179. procedure AddMapSimpleFields(aRecord: TSQLRecord; const aProps: array of TSQLPropInfo;
  6180. var aIndex: integer);
  6181. public
  6182. /// finalize the mapping
  6183. destructor Destroy; override;
  6184. /// map all columns of a TSQLTable to a record mapping
  6185. procedure Map(aRecord: TSQLRecord; aTable: TSQLTable; aCheckTableName: TSQLCheckTableName);
  6186. /// reset the mapping
  6187. // - is called e.g. by TSQLRecord.FillClose
  6188. // - will free any previous Table if necessary
  6189. // - will release TSQLRecordMany.Dest instances as set by TSQLRecord.FillPrepareMany()
  6190. procedure UnMap;
  6191. /// fill a TSQLRecord published properties from a TSQLTable row
  6192. // - use the mapping prepared with Map() method
  6193. function Fill(aRow: integer): Boolean; overload;
  6194. {$ifdef HASINLINE}inline;{$endif}
  6195. /// fill a TSQLRecord published properties from a TSQLTable row
  6196. // - use the mapping prepared with Map() method
  6197. // - aTableRow will point to the first column of the matching row
  6198. procedure Fill(aTableRow: PPUtf8CharArray); overload;
  6199. /// fill a TSQLRecord published properties from a TSQLTable row
  6200. // - overloaded method using a specified destination record to be filled
  6201. // - won't work with cross-reference mapping (FillPrepareMany)
  6202. // - use the mapping prepared with Map() method
  6203. // - aTableRow will point to the first column of the matching row
  6204. procedure Fill(aTableRow: PPUtf8CharArray; aDest: TSQLRecord); overload;
  6205. /// fill a TSQLRecord published properties from a TSQLTable row
  6206. // - overloaded method using a specified destination record to be filled
  6207. // - won't work with cross-reference mapping (FillPrepareMany)
  6208. // - use the mapping prepared with Map() method
  6209. function Fill(aRow: integer; aDest: TSQLRecord): Boolean; overload;
  6210. {$ifdef HASINLINE}inline;{$endif}
  6211. /// used to compute the updated field bits during a fill
  6212. // - will return Props.SimpleFieldsBits[soUpdate] if no fill is in process
  6213. procedure ComputeSetUpdatedFieldBits(Props: TSQLRecordProperties; out Bits: TSQLFieldBits);
  6214. /// return all mapped fields, or [] if nil
  6215. function TableMapFields: TSQLFieldBits;
  6216. /// the TSQLTable stated as FillPrepare() parameter
  6217. // - the internal temporary table is stored here for TSQLRecordMany
  6218. // - this instance is freed by TSQLRecord.Destroy if fTable.OwnerMustFree=true
  6219. property Table: TSQLTable read fTable;
  6220. /// the current Row during a Loop
  6221. property FillCurrentRow: integer read fFillCurrentRow;
  6222. /// equals TRUE if the instance was initialized via TSQLRecord.CreateJoined()
  6223. // TSQLRecord.CreateAndFillPrepareJoined()
  6224. // - it means that all nested TSQLRecord are pre-allocated instances,
  6225. // not trans-typed pointer(IDs)
  6226. property JoinedFields: boolean read GetJoinedFields;
  6227. end;
  6228. /// event signature triggered by TSQLRestBatch.OnWrite
  6229. // - also used by TSQLRestServer.RecordVersionSynchronizeSlave*() methods
  6230. TOnBatchWrite = procedure(Sender: TSQLRestBatch; Event: TSQLOccasion;
  6231. Table: TSQLRecordClass; const ID: TID; Value: TSQLRecord;
  6232. const ValueFields: TSQLFieldBits) of object;
  6233. /// used to store a BATCH sequence of writing operations
  6234. // - is used by TSQLRest to process BATCH requests using BatchSend() method,
  6235. // or TSQLRestClientURI for its Batch*() methods
  6236. // - but you can create your own stand-alone BATCH process, so that it will
  6237. // be able to make some transactional process - aka the "Unit Of Work" pattern
  6238. TSQLRestBatch = class
  6239. protected
  6240. fRest: TSQLRest;
  6241. fCalledWithinRest: boolean;
  6242. fBatch: TJSONSerializer;
  6243. fBatchFields: TSQLFieldBits;
  6244. fTable: TSQLRecordClass;
  6245. fTablePreviousSendData: TSQLRecordClass;
  6246. fTableIndex: integer;
  6247. fBatchCount: integer;
  6248. fDeletedRecordRef: TIDDynArray;
  6249. fDeletedCount: integer;
  6250. fAddCount: integer;
  6251. fUpdateCount: integer;
  6252. fDeleteCount: integer;
  6253. fAutomaticTransactionPerRow: cardinal;
  6254. fOptions: TSQLRestBatchOptions;
  6255. fOnWrite: TOnBatchWrite;
  6256. function GetCount: integer;
  6257. function GetSizeBytes: cardinal;
  6258. procedure SetExpandedJSONWriter(Props: TSQLRecordProperties;
  6259. ForceResetFields, withID: boolean; const WrittenFields: TSQLFieldBits);
  6260. public
  6261. /// begin a BATCH sequence to speed up huge database change
  6262. // - each call to normal Add/Update/Delete methods will create a Server request,
  6263. // therefore can be slow (e.g. if the remote server has bad ping timing)
  6264. // - start a BATCH sequence using this method, then call BatchAdd() BatchUpdate()
  6265. // or BatchDelete() methods to make some changes to the database
  6266. // - when BatchSend will be called, all the sequence transactions will be sent
  6267. // as one to the remote server, i.e. in one URI request
  6268. // - if BatchAbort is called instead, all pending BatchAdd/Update/Delete
  6269. // transactions will be aborted, i.e. ignored
  6270. // - expect one TSQLRecordClass as parameter, which will be used for the whole
  6271. // sequence (in this case, you can't mix classes in the same BATCH sequence)
  6272. // - if no TSQLRecordClass is supplied, the BATCH sequence will allow any
  6273. // kind of individual record in BatchAdd/BatchUpdate/BatchDelete
  6274. // - return TRUE on success, FALSE if aTable is incorrect or a previous BATCH
  6275. // sequence was already initiated
  6276. // - should normally be used inside a Transaction block: there is no automated
  6277. // TransactionBegin..Commit/RollBack generated in the BATCH sequence if
  6278. // you leave the default AutomaticTransactionPerRow=0 parameter - but
  6279. // this may be a concern with a lot of concurrent clients
  6280. // - you should better set AutomaticTransactionPerRow > 0 to execute all
  6281. // BATCH processes within an unique transaction grouped by a given number
  6282. // of rows, on the server side - set AutomaticTransactionPerRow=maxInt if
  6283. // you want one huge transaction, or set a convenient value (e.g. 10000)
  6284. // depending on the back-end database engine abilities, if you want to
  6285. // retain the transaction log file small enough for the database engine
  6286. // - BatchOptions could be set to tune the SQL execution, e.g. force INSERT
  6287. // OR IGNORE on internal SQLite3 engine
  6288. constructor Create(aRest: TSQLRest; aTable: TSQLRecordClass;
  6289. AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]);
  6290. /// finalize the BATCH instance
  6291. destructor Destroy; override;
  6292. /// reset the BATCH sequence so that you can re-use the same TSQLRestBatch
  6293. procedure Reset(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal=0;
  6294. Options: TSQLRestBatchOptions=[]); overload; virtual;
  6295. /// reset the BATCH sequence to its previous state
  6296. // - could be used to prepare a next chunk of values, after a call to
  6297. // TSQLRest.BatchSend
  6298. procedure Reset; overload;
  6299. /// create a new member in current BATCH sequence
  6300. // - work in BATCH mode: nothing is sent to the server until BatchSend call
  6301. // - returns the corresponding index in the current BATCH sequence, -1 on error
  6302. // - if SendData is true, content of Value is sent to the server as JSON
  6303. // - if ForceID is true, client sends the Value.ID field to use this ID for
  6304. // adding the record (instead of a database-generated ID)
  6305. // - if Value is TSQLRecordFTS3, Value.ID is stored to the virtual table
  6306. // - Value class MUST match the TSQLRecordClass used at BatchStart,
  6307. // or may be of any kind if no class was specified
  6308. // - BLOB fields are NEVER transmitted here, even if ForceBlobTransfert=TRUE
  6309. // - if CustomFields is left void, the simple fields will be used; otherwise,
  6310. // you can specify your own set of fields to be transmitted when SendData=TRUE
  6311. // (including BLOBs, even if they will be Base64-encoded within JSON content) -
  6312. // CustomFields could be computed by TSQLRecordProperties.FieldBitsFromCSV()
  6313. // or TSQLRecordProperties.FieldBitsFromRawUTF8(), or by setting ALL_FIELDS
  6314. // - this method will always compute and send TCreateTime/TModTime fields
  6315. function Add(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false;
  6316. const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): integer;
  6317. /// update a member in current BATCH sequence
  6318. // - work in BATCH mode: nothing is sent to the server until BatchSend call
  6319. // - returns the corresponding index in the current BATCH sequence, -1 on error
  6320. // - Value class MUST match the TSQLRecordClass used at BatchStart,
  6321. // or may be of any kind if no class was specified
  6322. // - BLOB fields are NEVER transmitted here, even if ForceBlobTransfert=TRUE
  6323. // - if Value has an opened FillPrepare() mapping, only the mapped fields
  6324. // will be updated (and also ID and TModTime fields) - FillPrepareMany() is
  6325. // not handled yet (all simple fields will be updated)
  6326. // - if CustomFields is left void, the simple fields will be used, or the
  6327. // fields retrieved via a previous FillPrepare() call; otherwise, you can
  6328. // specify your own set of fields to be transmitted (including BLOBs, even
  6329. // if they will be Base64-encoded within the JSON content) - CustomFields
  6330. // could be computed by TSQLRecordProperties.FieldBitsFromCSV()
  6331. // or TSQLRecordProperties.FieldBitsFromRawUTF8()
  6332. // - this method will always compute and send any TModTime fields, unless
  6333. // DoNotAutoComputeFields is set to true
  6334. function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[];
  6335. DoNotAutoComputeFields: boolean=false): integer; overload; virtual;
  6336. /// update a member in current BATCH sequence
  6337. // - work in BATCH mode: nothing is sent to the server until BatchSend call
  6338. // - is an overloaded method to Update(Value,FieldBitsFromCSV())
  6339. function Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8;
  6340. DoNotAutoComputeFields: boolean=false): integer; overload;
  6341. /// delete a member in current BATCH sequence
  6342. // - work in BATCH mode: nothing is sent to the server until BatchSend call
  6343. // - returns the corresponding index in the current BATCH sequence, -1 on error
  6344. // - deleted record class is the TSQLRecordClass used at BatchStart()
  6345. // call: it will fail if no class was specified for this BATCH sequence
  6346. function Delete(ID: TID): integer; overload;
  6347. /// delete a member in current BATCH sequence
  6348. // - work in BATCH mode: nothing is sent to the server until BatchSend call
  6349. // - returns the corresponding index in the current BATCH sequence, -1 on error
  6350. // - with this overloaded method, the deleted record class is specified:
  6351. // no TSQLRecordClass shall have been set at BatchStart() call
  6352. function Delete(Table: TSQLRecordClass; ID: TID): integer; overload;
  6353. /// allow to append some JSON content to the internal raw buffer
  6354. // - could be used to emulate Add/Update/Delete
  6355. // - FullRow=TRUE would increment the global Count
  6356. function RawAppend(FullRow: boolean=true): TTextWriter;
  6357. /// allow to append some JSON content to the internal raw buffer for a POST
  6358. // - could be used to emulate Add() with an already pre-computed JSON object
  6359. procedure RawAdd(const SentData: RawUTF8);
  6360. /// allow to append some JSON content to the internal raw buffer for a PUT
  6361. // - could be used to emulate Update() with an already pre-computed JSON object
  6362. procedure RawUpdate(const SentData: RawUTF8; ID: TID);
  6363. /// close a BATCH sequence started by Start method
  6364. // - Data is ready to be supplied to TSQLRest.BatchSend() overloaded method
  6365. // - will also notify the TSQLRest.Cache for all deleted IDs
  6366. // - you should not have to call it in normal use cases
  6367. function PrepareForSending(out Data: RawUTF8): boolean; virtual;
  6368. /// read only access to the associated TSQLRest instance
  6369. property Rest: TSQLRest read fRest;
  6370. /// retrieve the current number of pending transactions in the BATCH sequence
  6371. property Count: integer read GetCount;
  6372. /// retrieve the current JSON size of pending transaction in the BATCH sequence
  6373. property SizeBytes: cardinal read GetSizeBytes;
  6374. /// read only access to the main associated TSQLRecord class (if any)
  6375. property Table: TSQLRecordClass read fTable;
  6376. /// how many times Add() has been called for this BATCH process
  6377. property AddCount: integer read fAddCount;
  6378. /// how many times Update() has been called for this BATCH process
  6379. property UpdateCount: integer read fUpdateCount;
  6380. /// how many times Delete() has been called for this BATCH process
  6381. property DeleteCount: integer read fDeleteCount;
  6382. /// this event handler will be triggerred by each Add/Update/Delete method
  6383. property OnWrite: TOnBatchWrite read fOnWrite write fOnWrite;
  6384. end;
  6385. /// thread-safe class to store a BATCH sequence of writing operations
  6386. TSQLRestBatchLocked = class(TSQLRestBatch)
  6387. protected
  6388. fTix: Int64;
  6389. fSafe: TSynLocker;
  6390. public
  6391. /// initialize the BATCH instance
  6392. constructor Create(aRest: TSQLRest; aTable: TSQLRecordClass;
  6393. AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]);
  6394. /// finalize the BATCH instance
  6395. destructor Destroy; override;
  6396. /// reset the BATCH sequence so that you can re-use the same TSQLRestBatch
  6397. procedure Reset(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal=0;
  6398. Options: TSQLRestBatchOptions=[]); override;
  6399. /// access to the locking methods of this instance
  6400. // - use Safe.Lock/TryLock with a try ... finally Safe.Unlock block
  6401. property Safe: TSynLocker read fSafe;
  6402. /// property set to the current GetTickCount64 value when Reset is called
  6403. property ResetTix: Int64 read fTix write fTix;
  6404. end;
  6405. /// root class for defining and mapping database records
  6406. // - inherits a class from TSQLRecord, and add published properties to describe
  6407. // the table columns (see TPropInfo for SQL and Delphi type mapping/conversion)
  6408. // - this published properties can be auto-filled from TSQLTable answer with
  6409. // FillPrepare() and FillRow(), or FillFrom() with TSQLTable or JSON data
  6410. // - these published properties can be converted back into UTF-8 encoded SQL
  6411. // source with GetSQLValues or GetSQLSet or into JSON format with GetJSONValues
  6412. // - BLOB fields are decoded to auto-freeing TSQLRawBlob properties
  6413. // - any published property defined as a T*ObjArray dynamic array storage
  6414. // of persistents (via TJSONSerializer.RegisterObjArrayForJSON) will be freed
  6415. TSQLRecord = class(TObject)
  6416. { note that every TSQLRecord has an Instance size of 20 bytes for private and
  6417. protected fields (such as fID or fProps e.g.) }
  6418. protected
  6419. /// used by FillPrepare() and corresponding Fill*() methods
  6420. fFill: TSQLRecordFill;
  6421. /// internal properties getters (using fProps data for speed)
  6422. function GetHasBlob: boolean;
  6423. function GetSimpleFieldCount: integer;
  6424. function GetFillCurrentRow: integer;
  6425. function GetTable: TSQLTable;
  6426. protected
  6427. fInternalState: cardinal;
  6428. fID: TID;
  6429. /// virtual class method to be overridden to register some custom properties
  6430. // - do nothing by default, but allow inherited classes to define some
  6431. // properties, by adding some TSQLPropInfo instances to Props.Fields list,
  6432. // or calling Props.RegisterCustomFixedSizeRecordProperty() or
  6433. // Props.RegisterCustomRTTIRecordProperty() methods
  6434. // - can also be used to specify a custom text collation, by calling
  6435. // Props.SetCustomCollationForAll() or SetCustomCollation() methods
  6436. // - do not call RecordProps from here (e.g. by calling AddFilter*): it
  6437. // woult trigger a stack overflow, since at this state Props is not stored -
  6438. // but rather use InternalDefineModel class method
  6439. class procedure InternalRegisterCustomProperties(Props: TSQLRecordProperties); virtual;
  6440. /// virtual class method to be overridden to define some record-level modeling
  6441. // - do nothing by default, but allow inherited classes to define some
  6442. // process which would take place after TSQLRecordProperties initialization
  6443. // - this may be the place e.g. to call AddFilter*() methods, if you do not
  6444. // want those to be written "in stone", and not manually when creating the
  6445. // TSQLModel instance
  6446. class procedure InternalDefineModel(Props: TSQLRecordProperties); virtual;
  6447. {$ifdef MSWINDOWS}{$ifdef HASINLINE}
  6448. public
  6449. {$endif}{$endif}
  6450. /// trick to get the ID even in case of a sftID published property
  6451. function GetID: TID;
  6452. {$ifdef MSWINDOWS}{$ifdef HASINLINE}inline;{$endif}{$endif}
  6453. /// trick to typecast the ID on 64-bit platform
  6454. function GetIDAsPointer: pointer;
  6455. {$ifdef MSWINDOWS}{$ifdef HASINLINE}inline;{$endif}{$endif}
  6456. public
  6457. /// direct access to the TSQLRecord properties from RTTI
  6458. // - TSQLRecordProperties is faster than e.g. the class function FieldProp()
  6459. // - use internal the unused vmtAutoTable VMT entry to fast retrieve of a
  6460. // class variable which is unique for each class ("class var" is unique only
  6461. // for the class within it is defined, and we need a var for each class:
  6462. // so even Delphi XE syntax is not powerful enough for our purpose, and the
  6463. // vmtAutoTable trick if very fast, and works with all versions of Delphi -
  6464. // including 64-bit target)
  6465. class function RecordProps: TSQLRecordProperties;
  6466. {$ifdef FPC_OR_PUREPASCAL}{$ifdef HASINLINE}inline;{$endif}{$endif}
  6467. /// the Table name in the database, associated with this TSQLRecord class
  6468. // - 'TSQL' or 'TSQLRecord' chars are trimmed at the beginning of the ClassName
  6469. // - or the ClassName is returned as is, if no 'TSQL' or 'TSQLRecord' at first
  6470. // - is just a wrapper around RecordProps.SQLTableName
  6471. class function SQLTableName: RawUTF8;
  6472. {$ifdef HASINLINE}inline;{$endif}
  6473. /// register a custom filter (transformation) or validate to the
  6474. // TSQLRecord class for a specified field
  6475. // - this will be used by TSQLRecord.Filter and TSQLRecord.Validate
  6476. // methods (in default implementation)
  6477. // - will raise an EModelException on failure
  6478. // - this function is just a wrapper around RecordProps.AddFilterOrValidate
  6479. class procedure AddFilterOrValidate(const aFieldName: RawUTF8;
  6480. aFilter: TSynFilterOrValidate);
  6481. /// register a TSynFilterTrim and a TSynValidateText filters so that
  6482. // the specified fields, after space trimming, won't be void
  6483. class procedure AddFilterNotVoidText(const aFieldNames: array of RawUTF8);
  6484. /// register a TSynFilterTrim and a TSynValidateText filters so that
  6485. // all text fields, after space trimming, won't be void
  6486. // - will only affect RAWTEXT_FIELDS
  6487. class procedure AddFilterNotVoidAllTextFields;
  6488. /// protect several TSQLRecord local variable instances
  6489. // - specified as localVariable/recordClass pairs
  6490. // - is a wrapper around TAutoFree.Several(...) constructor
  6491. // - be aware that it won't implement a full ARC memory model, but may be
  6492. // just used to avoid writing some try ... finally blocks on local variables
  6493. // - use with caution, only on well defined local scope
  6494. // - you may write for instance:
  6495. // ! var info: TSQLBlogInfo;
  6496. // ! article: TSQLArticle;
  6497. // ! comment: TSQLComment;
  6498. // ! begin
  6499. // ! TSQLRecord.AutoFree([ // avoid several try..finally
  6500. // ! @info,TSQLBlogInfo, @article,TSQLArticle, @comment,TSQLComment]);
  6501. // ! .... now you can use info, article or comment
  6502. // ! end; // will call info.Free article.Free and comment.Free
  6503. // - warning: under FPC, you should assign the result of this method to a local
  6504. // IAutoFree variable - see bug http://bugs.freepascal.org/view.php?id=26602
  6505. class function AutoFree(varClassPairs: array of pointer): IAutoFree; overload;
  6506. /// protect one TSQLRecord local variable instance
  6507. // - be aware that it won't implement a full ARC memory model, but may be
  6508. // just used to avoid writing some try ... finally blocks on local variables
  6509. // - use with caution, only on well defined local scope
  6510. // - you may write for instance:
  6511. // ! var info: TSQLBlogInfo;
  6512. // ! begin
  6513. // ! TSQLBlogInfo.AutoFree(info);
  6514. // ! .... now you can use info
  6515. // ! end; // will call info.Free
  6516. // - warning: under FPC, you should assign the result of this method to a local
  6517. // IAutoFree variable - see bug http://bugs.freepascal.org/view.php?id=26602
  6518. class function AutoFree(var localVariable): IAutoFree; overload;
  6519. /// read and protect one TSQLRecord local variable instance
  6520. // - be aware that it won't implement a full ARC memory model, but may be
  6521. // just used to avoid writing some try ... finally blocks on local variables
  6522. // - use with caution, only on well defined local scope
  6523. // - warning: under FPC, you should assign the result of this method to a local
  6524. // IAutoFree variable - see bug http://bugs.freepascal.org/view.php?id=26602
  6525. class function AutoFree(var localVariable; Rest: TSQLRest; ID: TID): IAutoFree; overload;
  6526. /// get the captions to be used for this class
  6527. // - if Action is nil, return the caption of the table name
  6528. // - if Action is not nil, return the caption of this Action (lowercase left-trimed)
  6529. // - return "string" type, i.e. UnicodeString for Delphi 2009+
  6530. // - internally call UnCamelCase() then System.LoadResStringTranslate() if available
  6531. // - ForHint is set to TRUE when the record caption name is to be displayed inside
  6532. // the popup hint of a button (i.e. the name must be fully qualified, not
  6533. // the default short version)
  6534. // - is not part of TSQLRecordProperties because has been declared as virtual
  6535. class function CaptionName(Action: PRawUTF8=nil; ForHint: boolean=false): string; virtual;
  6536. /// get the captions to be used for this class
  6537. // - just a wrapper calling CaptionName() virtual method, from a ShortString pointer
  6538. class function CaptionNameFromRTTI(Action: PShortString): string;
  6539. /// virtual method called when the associated table is created in the database
  6540. // - if FieldName is '', initialization regarding all fields must be made;
  6541. // if FieldName is specified, initialization regarding this field must be processed
  6542. // - override this method in order to initialize indexs or create default records
  6543. // - by default, create indexes for all TRecordReference properties, and
  6544. // for all TSQLRecord inherited properties (i.e. of sftID type, that is
  6545. // an INTEGER field containing the ID of the pointing record)
  6546. // - the options specified at CreateMissingTables() are passed to this method
  6547. // - is not part of TSQLRecordProperties because has been declared as virtual
  6548. class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
  6549. Options: TSQLInitializeTableOptions); virtual;
  6550. /// filter/transform the specified fields values of the TSQLRecord instance
  6551. // - by default, this will perform all TSynFilter as registered by
  6552. // [RecordProps.]AddFilterOrValidate()
  6553. // - inherited classes may add some custom filtering/transformation here, if
  6554. // it's not needed nor mandatory to create a new TSynFilter class type: in
  6555. // this case, the function has to return TRUE if the filtering took place,
  6556. // and FALSE if any default registered TSynFilter must be processed
  6557. // - the default aFields parameter will process all fields
  6558. function Filter(const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]): boolean; overload; virtual;
  6559. /// filter/transform the specified fields values of the TSQLRecord instance
  6560. // - this version will call the overloaded Filter() method above
  6561. // - return TRUE if all field names were correct and processed, FALSE otherwise
  6562. function Filter(const aFields: array of RawUTF8): boolean; overload;
  6563. /// validate the specified fields values of the current TSQLRecord instance
  6564. // - by default, this will perform all TSynValidate as registered by
  6565. // [RecordProps.]AddFilterOrValidate()
  6566. // - it will also check if any UNIQUE field value won't be duplicated
  6567. // - inherited classes may add some custom validation here, if it's not needed
  6568. // nor mandatory to create a new TSynValidate class type: in this case, the
  6569. // function has to return an explicit error message (as a generic VCL string)
  6570. // if the custom validation failed, or '' if the validation was successful:
  6571. // in this later case, all default registered TSynValidate are processed
  6572. // - the default aFields parameter will process all fields
  6573. // - if aInvalidFieldIndex is set, it will contain the first invalid field
  6574. // index found
  6575. // - caller SHOULD always call the Filter() method before calling Validate()
  6576. function Validate(aRest: TSQLRest; const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1];
  6577. aInvalidFieldIndex: PInteger=nil; aValidator: PSynValidate=nil): string; overload; virtual;
  6578. /// validate the specified fields values of the current TSQLRecord instance
  6579. // - this version will call the overloaded Validate() method above
  6580. // - returns '' if all field names were correct and processed, or an
  6581. // explicit error message (translated in the current language) on error
  6582. // - if aInvalidFieldIndex is set, it will contain the first invalid field index
  6583. function Validate(aRest: TSQLRest; const aFields: array of RawUTF8;
  6584. aInvalidFieldIndex: PInteger=nil; aValidator: PSynValidate=nil): string; overload;
  6585. /// filter (transform) then validate the specified fields values of the TSQLRecord
  6586. // - this version will call the overloaded Filter() and Validate() methods
  6587. // and display the faulty field name at the beginning of the error message
  6588. // - returns true if all field names were correct and processed, or false
  6589. // and an explicit error message (translated in the current language) on error
  6590. function FilterAndValidate(aRest: TSQLRest; out aErrorMessage: string;
  6591. const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1];
  6592. aValidator: PSynValidate=nil): boolean; overload;
  6593. /// filter (transform) then validate the specified fields values of the TSQLRecord
  6594. // - this version will call the overloaded Filter() and Validate() methods
  6595. // and return '' on validation success, or an error message with the faulty
  6596. // field names at the beginning
  6597. function FilterAndValidate(aRest: TSQLRest;
  6598. const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1];
  6599. aValidator: PSynValidate=nil): RawUTF8; overload;
  6600. /// should modify the record content before writing to the Server
  6601. // - this default implementation will update any sftModTime / TModTime,
  6602. // sftCreateTime / TCreateTime and sftSessionUserID / TSessionUserID
  6603. // properties content with the exact server time stamp
  6604. // - you may override this method e.g. for custom calculated fields
  6605. // - note that this is computed only on the Client side, before sending
  6606. // back the content to the remote Server: therefore, TModTime / TCreateTime
  6607. // fields are a pure client ORM feature - it won't work directly at REST level
  6608. procedure ComputeFieldsBeforeWrite(aRest: TSQLRest; aOccasion: TSQLEvent); virtual;
  6609. /// this constructor initializes the record
  6610. // - auto-instanciate any TSQLRecordMany instance defined in published properties
  6611. // - override this method if you want to use some internal objects (e.g.
  6612. // TStringList or TCollection as published property)
  6613. constructor Create; overload; virtual;
  6614. /// this constructor initializes the record and set the simple fields
  6615. // with the supplied values
  6616. // - the aSimpleFields parameters must follow explicitely the order of
  6617. // published properties of the aTable class, excepting the TSQLRawBlob and
  6618. // TSQLRecordMany kind (i.e. only so called "simple fields") - in
  6619. // particular, parent properties must appear first in the list
  6620. // - the aSimpleFields must have exactly the same count of parameters as
  6621. // there are "simple fields" in the published properties
  6622. // - will raise an EORMException in case of wrong supplied values
  6623. constructor Create(const aSimpleFields: array of const; aID: TID); overload;
  6624. /// this constructor initializes the object as above, and fills its content
  6625. // from a client or server connection
  6626. // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock
  6627. // the corresponding record, then retrieve its content; caller has to call
  6628. // UnLock() method after Value usage, to release the record
  6629. constructor Create(aClient: TSQLRest; aID: TID;
  6630. ForUpdate: boolean=false); overload;
  6631. /// this constructor initializes the object and fills its content from a client
  6632. // or server connection, from a TSQLRecord published property content
  6633. // - is just a wrapper around Create(aClient,PtrInt(aPublishedRecord))
  6634. // or Create(aClient,aPublishedRecord.ID)
  6635. // - a published TSQLRecord property is not a class instance, but a typecast to
  6636. // TObject(RecordID) - you can also use its ID property
  6637. // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock
  6638. // the corresponding record, then retrieve its content; caller has to call
  6639. // UnLock() method after Value usage, to release the record
  6640. constructor Create(aClient: TSQLRest; aPublishedRecord: TSQLRecord;
  6641. ForUpdate: boolean=false); overload;
  6642. /// this constructor initializes the object as above, and fills its content
  6643. // from a client or server connection, using a specified WHERE clause
  6644. // - the WHERE clause should use inlined parameters (like 'Name=:('Arnaud'):')
  6645. // for better server speed - note that you can use FormatUTF8() as such:
  6646. // ! aRec := TSQLMyRec.Create(Client,FormatUTF8('Salary>? AND Salary<?',[],[1000,2000]));
  6647. // or call the overloaded contructor with BoundsSQLWhere array of parameters
  6648. constructor Create(aClient: TSQLRest; const aSQLWhere: RawUTF8); overload;
  6649. /// this constructor initializes the object as above, and fills its content
  6650. // from a client or server connection, using a specified WHERE clause
  6651. // with parameters
  6652. // - for better server speed, the WHERE clause should use bound parameters
  6653. // identified as '?' in the FormatSQLWhere statement, which is expected to
  6654. // follow the order of values supplied in BoundsSQLWhere open array - use
  6655. // DateToSQL/DateTimeToSQL for TDateTime, or directly any integer / double /
  6656. // currency / RawUTF8 values to be bound to the request as parameters
  6657. // - note that this method prototype changed with revision 1.17 of the
  6658. // framework: array of const used to be ParamsSQLWhere and '%' in the
  6659. // FormatSQLWhere statement, whereas it now expects bound parameters as '?'
  6660. constructor Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  6661. const BoundsSQLWhere: array of const); overload;
  6662. /// this constructor initializes the object as above, and fills its content
  6663. // from a client or server connection, using a specified WHERE clause
  6664. // with parameters
  6665. // - the FormatSQLWhere clause will replace all '%' chars with the supplied
  6666. // ParamsSQLWhere[] values, and all '?' chars with BoundsSQLWhere[] values,
  6667. // as :(...): inlined parameters - you should either call:
  6668. // ! Rec := TSQLMyRecord.Create(aClient,'Count=:(%):'[aCount],[]);
  6669. // or (letting the inlined parameters being computed by FormatUTF8)
  6670. // ! Rec := TSQLMyRecord.Create(aClient,'Count=?',[],[aCount]);
  6671. // or even better, using the other Create overloaded constructor:
  6672. // ! Rec := TSQLMyRecord.Create(aClient,'Count=?',[aCount]);
  6673. // - using '?' and BoundsSQLWhere[] is perhaps more readable in your code, and
  6674. // will in all case create a request with :(..): inline parameters, with
  6675. // automatic RawUTF8 quoting if necessary
  6676. constructor Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  6677. const ParamsSQLWhere, BoundsSQLWhere: array of const); overload;
  6678. /// this constructor initializes the object as above, and fills its content
  6679. // from a supplied JSON content
  6680. // - is a wrapper around Create + FillFrom() methods
  6681. // - use JSON data, as exported by GetJSONValues(), expanded or not
  6682. // - make an internal copy of the JSONTable RawUTF8 before calling
  6683. // FillFrom() below
  6684. constructor CreateFrom(const JSONRecord: RawUTF8); overload;
  6685. /// this constructor initializes the object as above, and fills its content
  6686. // from a supplied JSON buffer
  6687. // - is a wrapper around Create + FillFrom() methods
  6688. // - use JSON data, as exported by GetJSONValues(), expanded or not
  6689. // - the data inside P^ is modified (unescaped and transformed in-place):
  6690. // don't call CreateFrom(pointer(JSONRecord)) but CreateFrom(JSONRecord) which
  6691. // makes a temporary copy of the JSONRecord text variable before parsing
  6692. constructor CreateFrom(P: PUTF8Char); overload;
  6693. {$ifndef NOVARIANTS}
  6694. /// this constructor initializes the object as above, and fills its content
  6695. // from a supplied TDocVariant object document
  6696. // - is a wrapper around Create + FillFrom() methods
  6697. constructor CreateFrom(const aDocVariant: variant); overload;
  6698. {$endif}
  6699. /// this constructor initializes the object as above, and prepares itself to
  6700. // loop through a statement using a specified WHERE clause
  6701. // - this method creates a TSQLTableJSON, retrieves all records corresponding
  6702. // to the WHERE clause, then call FillPrepare - previous Create(aClient)
  6703. // methods retrieve only one record, this one more multiple rows
  6704. // - you should then loop for all rows using 'while Rec.FillOne do ...'
  6705. // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
  6706. // - the WHERE clause should use inlined parameters (like 'Name=:('Arnaud'):')
  6707. // for better server speed - note that you can use FormatUTF8() as such:
  6708. // ! aRec := TSQLMyRec.CreateAndFillPrepare(Client,FormatUTF8('Salary>? AND Salary<?',[],[1000,2000]));
  6709. // or call the overloaded CreateAndFillPrepare() contructor directly with
  6710. // BoundsSQLWhere array of parameters
  6711. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  6712. // - default aCustomFieldsCSV='' will retrieve all simple table fields
  6713. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  6714. // - aCustomFieldsCSV can also be set to a CSV field list to retrieve only
  6715. // the needed fields, and save remote bandwidth - note that any later
  6716. // Update() will update all simple fields, so potentially with wrong
  6717. // values; but BatchUpdate() can be safely used since it will
  6718. constructor CreateAndFillPrepare(aClient: TSQLRest; const aSQLWhere: RawUTF8;
  6719. const aCustomFieldsCSV: RawUTF8=''); overload;
  6720. /// this constructor initializes the object as above, and prepares itself to
  6721. // loop through a statement using a specified WHERE clause
  6722. // - this method creates a TSQLTableJSON, retrieves all records corresponding
  6723. // to the WHERE clause, then call FillPrepare - previous Create(aClient)
  6724. // methods retrieve only one record, this one more multiple rows
  6725. // - you should then loop for all rows using 'while Rec.FillOne do ...'
  6726. // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
  6727. // - for better server speed, the WHERE clause should use bound parameters
  6728. // identified as '?' in the FormatSQLWhere statement, which is expected to
  6729. // follow the order of values supplied in BoundsSQLWhere open array - use
  6730. // DateToSQL/DateTimeToSQL for TDateTime, or directly any integer / double /
  6731. // currency / RawUTF8 values to be bound to the request as parameters
  6732. // - note that this method prototype changed with revision 1.17 of the
  6733. // framework: array of const used to be ParamsSQLWhere and '%' in the
  6734. // FormatSQLWhere statement, whereas it now expects bound parameters as '?'
  6735. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  6736. // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
  6737. // you may need to access only one or several fields, and will save remote
  6738. // bandwidth by specifying the needed fields
  6739. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  6740. // - note that you should not use this aCustomFieldsCSV optional parameter if
  6741. // you want to Update the retrieved record content later, since any
  6742. // missing fields will be left with previous values - but BatchUpdate() can be
  6743. // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
  6744. constructor CreateAndFillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  6745. const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''); overload;
  6746. /// this constructor initializes the object as above, and prepares itself to
  6747. // loop through a statement using a specified WHERE clause
  6748. // - this method creates a TSQLTableJSON, retrieves all records corresponding
  6749. // to the WHERE clause, then call FillPrepare - previous Create(aClient)
  6750. // methods retrieve only one record, this one more multiple rows
  6751. // - you should then loop for all rows using 'while Rec.FillOne do ...'
  6752. // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
  6753. // - the FormatSQLWhere clause will replace all '%' chars with the supplied
  6754. // ParamsSQLWhere[] supplied values, and bind all '?' chars as parameters
  6755. // with BoundsSQLWhere[] values
  6756. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  6757. // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
  6758. // you may need to access only one or several fields, and will save remote
  6759. // bandwidth by specifying the needed fields
  6760. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  6761. // - note that you should not use this aCustomFieldsCSV optional parameter if
  6762. // you want to Update the retrieved record content later, since any
  6763. // missing fields will be left with previous values - but BatchUpdate() can be
  6764. // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
  6765. constructor CreateAndFillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  6766. const ParamsSQLWhere, BoundsSQLWhere: array of const;
  6767. const aCustomFieldsCSV: RawUTF8=''); overload;
  6768. /// this constructor initializes the object as above, and prepares itself to
  6769. // loop through a given list of IDs
  6770. // - this method creates a TSQLTableJSON, retrieves all records corresponding
  6771. // to the specified IDs, then call FillPrepare - previous Create(aClient)
  6772. // methods retrieve only one record, this one more multiple rows
  6773. // - you should then loop for all rows using 'while Rec.FillOne do ...'
  6774. // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
  6775. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  6776. // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
  6777. // you may need to access only one or several fields, and will save remote
  6778. // bandwidth by specifying the needed fields
  6779. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  6780. // - note that you should not use this aCustomFieldsCSV optional parameter if
  6781. // you want to Update the retrieved record content later, since any
  6782. // missing fields will be left with previous values - but BatchUpdate() can be
  6783. // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
  6784. constructor CreateAndFillPrepare(aClient: TSQLRest; const aIDs: array of Int64;
  6785. const aCustomFieldsCSV: RawUTF8=''); overload;
  6786. /// this constructor initializes the object, and prepares itself to loop
  6787. // through a specified JSON table
  6788. // - this method creates a TSQLTableJSON, fill it with the supplied JSON buffer,
  6789. // then call FillPrepare - previous Create(aClient) methods retrieve only
  6790. // one record, this one more multiple rows
  6791. // - you should then loop for all rows using 'while Rec.FillOne do ...'
  6792. // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
  6793. constructor CreateAndFillPrepare(const aJSON: RawUTF8); overload;
  6794. /// this constructor initializes the object from its ID, including all
  6795. // nested TSQLRecord properties, through a JOINed statement
  6796. // - by default, Create(aClient,aID) will return only the one-to-one
  6797. // nested TSQLRecord published properties IDs trans-typed as pointer - this
  6798. // constructor allow to retrieve the nested values in one statement
  6799. // - use this constructor if you want all TSQLRecord published properties to
  6800. // be allocated, and loaded with the corresponding values
  6801. // - Free/Destroy will release these instances
  6802. // - warning: if you call Update() after it, only the main object will be
  6803. // updated, not the nested TSQLRecord properties
  6804. constructor CreateJoined(aClient: TSQLRest; aID: TID);
  6805. /// this constructor initializes the object, and prepares itself to loop
  6806. // nested TSQLRecord properties, through a JOINed statement and a WHERE clause
  6807. // - by default, CreateAndFillPrepare() will return only the one-to-one
  6808. // nested TSQLRecord published properties IDs trans-typed as pointer - this
  6809. // constructor allow to retrieve the nested values in one statement
  6810. // - this method creates a TSQLTableJSON, fill it with the supplied JSON buffer,
  6811. // then call FillPrepare - previous CreateJoined() method retrieve only
  6812. // one record, this one more multiple rows
  6813. // - you should then loop for all rows using 'while Rec.FillOne do ...'
  6814. // - use this constructor if you want all TSQLRecord published properties to
  6815. // be allocated, and loaded with the corresponding values
  6816. // - Free/Destroy will release these instances
  6817. // - warning: if you call Update() after it, only the main object will be
  6818. // updated, not the nested TSQLRecord properties
  6819. constructor CreateAndFillPrepareJoined(aClient: TSQLRest;
  6820. const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const);
  6821. /// this constructor initializes the object including all TSQLRecordMany properties,
  6822. // and prepares itself to loop through a JOINed statement
  6823. // - the created instance will have all its TSQLRecordMany Dest property allocated
  6824. // with proper instance (and not only pointer(DestID) e.g.), ready to be
  6825. // consumed during a while FillOne do... loop (those instances will be
  6826. // freed by TSQLRecord.FillClose or Destroy) - and the Source property
  6827. // won't contain pointer(SourceID) but the main TSQLRecord instance
  6828. // - the aFormatSQLJoin clause will define a WHERE clause for an automated
  6829. // JOINed statement, including TSQLRecordMany published properties (and
  6830. // their nested properties)
  6831. // - a typical use could be the following:
  6832. // ! aProd := TSQLProduct.CreateAndFillPrepareMany(Database,
  6833. // ! 'Owner=? and Categories.Dest.Name=? and (Sizes.Dest.Name=? or Sizes.Dest.Name=?)',[],
  6834. // ! ['mark','for boy','small','medium']);
  6835. // ! if aProd<>nil then
  6836. // ! try
  6837. // ! while aProd.FillOne do
  6838. // ! // here e.g. aProd.Categories.Dest are instantied (and Categories.Source=aProd)
  6839. // ! writeln(aProd.Name,' ',aProd.Owner,' ',aProd.Categories.Dest.Name,' ',aProd.Sizes.Dest.Name);
  6840. // ! // you may also use aProd.FillTable to fill a grid, e.g.
  6841. // ! // (do not forget to set aProd.FillTable.OwnerMustFree := false)
  6842. // ! finally
  6843. // ! aProd.Free; // will also free aProd.Categories/Sizes instances
  6844. // ! end;
  6845. // this will execute a JOINed SELECT statement similar to the following:
  6846. // $ select p.*, c.*, s.*
  6847. // $ from Product p, Category c, Categories cc, Size s, Sizes ss
  6848. // $ where c.id=cc.dest and cc.source=p.id and
  6849. // $ s.id=ss.dest and ss.source=p.id and
  6850. // $ p.Owner='mark' and c.Name='for boy' and (s.Name='small' or s.Name='medium')
  6851. // - you SHALL call explicitely the FillClose method before using any
  6852. // methods of nested TSQLRecordMany instances which may override the Dest
  6853. // instance content (e.g. ManySelect) to avoid any GPF
  6854. // - the aFormatSQLJoin clause will replace all '%' chars with the supplied
  6855. // aParamsSQLJoin[] supplied values, and bind all '?' chars as bound
  6856. // parameters with aBoundsSQLJoin[] values
  6857. constructor CreateAndFillPrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8;
  6858. const aParamsSQLJoin, aBoundsSQLJoin: array of const);
  6859. /// this method create a clone of the current record, with same ID and properties
  6860. // - copy all COPIABLE_FIELDS, i.e. all fields excluding tftMany (because
  6861. // those fields don't contain any data, but a TSQLRecordMany instance
  6862. // which allow to access to the pivot table data)
  6863. // - you can override this method to allow custom copy of the object,
  6864. // including (or not) published properties copy
  6865. function CreateCopy: TSQLRecord; overload; virtual;
  6866. /// this method create a clone of the current record, with same ID and properties
  6867. // - overloaded method to copy the specified properties
  6868. function CreateCopy(const CustomFields: TSQLFieldBits): TSQLRecord; overload;
  6869. /// release the associated memory
  6870. // - in particular, release all TSQLRecordMany instance created by the
  6871. // constructor of this TSQLRecord
  6872. destructor Destroy; override;
  6873. /// return the UTF-8 encoded SQL source to create the table containing the
  6874. // published fields of a TSQLRecord child
  6875. // - a 'ID INTEGER PRIMARY KEY' field is always created first (mapping
  6876. // SQLite3 RowID)
  6877. // - AnsiString are created as TEXT COLLATE NOCASE (fast SQLite3 7bits compare)
  6878. // - RawUnicode and RawUTF8 are created as TEXT COLLATE SYSTEMNOCASE
  6879. // (i.e. use our fast UTF8IComp() for comparaison)
  6880. // - TDateTime are created as TEXT COLLATE ISO8601
  6881. // (which calls our very fast ISO TEXT to Int64 conversion routine)
  6882. // - an individual bit set in UniqueField forces the corresponding field to
  6883. // be marked as UNIQUE (an unique index is automaticaly created on the specified
  6884. // column); use TSQLModel fIsUnique[] array, which set the bits values
  6885. // to 1 if a property field was published with "stored AS_UNIQUE"
  6886. // (i.e. "stored false")
  6887. // - this method will handle TSQLRecordFTS* classes like FTS* virtual tables,
  6888. // TSQLRecordRTree as RTREE virtual table, and TSQLRecordVirtualTable*ID
  6889. // classes as corresponding Delphi designed virtual tables
  6890. // - is not part of TSQLRecordProperties because has been declared as virtual
  6891. // so that you could specify a custom SQL statement, per TSQLRecord type
  6892. // - anyway, don't call this method directly, but use TSQLModel.GetSQLCreate()
  6893. // - the aModel parameter is used to retrieve the Virtual Table module name,
  6894. // and can be ignored for regular (not virtual) tables
  6895. class function GetSQLCreate(aModel: TSQLModel): RawUTF8; virtual;
  6896. /// return the Class Type of the current TSQLRecord
  6897. function RecordClass: TSQLRecordClass;
  6898. {$ifdef PUREPASCAL} {$ifdef HASINLINE}inline;{$endif} {$endif}
  6899. /// return the RTTI property information for this record
  6900. function ClassProp: PClassProp;
  6901. {$ifdef PUREPASCAL} {$ifdef HASINLINE}inline;{$endif} {$endif}
  6902. /// return the TRecordReference Int64 value pointing to this record
  6903. function RecordReference(Model: TSQLModel): TRecordReference;
  6904. /// return the UTF-8 encoded SQL source to INSERT the values contained
  6905. // in the current published fields of a TSQLRecord child
  6906. // - only simple fields name (i.e. not TSQLRawBlob/TSQLRecordMany) are updated:
  6907. // BLOB fields are ignored (use direct update via dedicated methods instead)
  6908. // - format is '(COL1, COL2) VALUES ('VAL1', 'VAL2')' if some column was ignored
  6909. // (BLOB e.g.)
  6910. // - format is 'VALUES ('VAL1', 'VAL2')' if all columns values are available
  6911. // - is not used by the ORM (do not use prepared statements) - only here
  6912. // for conveniency
  6913. function GetSQLValues: RawUTF8;
  6914. /// return the UTF-8 encoded SQL source to UPDATE the values contained
  6915. // in the current published fields of a TSQLRecord child
  6916. // - only simple fields name (i.e. not TSQLRawBlob/TSQLRecordMany) are retrieved:
  6917. // BLOB fields are ignored (use direct access via dedicated methods instead)
  6918. // - format is 'COL1='VAL1', COL2='VAL2''
  6919. // - is not used by the ORM (do not use prepared statements) - only here
  6920. // for conveniency
  6921. function GetSQLSet: RawUTF8;
  6922. /// return the UTF-8 encoded JSON objects for the values of this TSQLRecord
  6923. // - layout and fields should have been set at TJSONSerializer construction:
  6924. // to append some content to an existing TJsonSerializer, call the
  6925. // AppendAsJsonObject() method
  6926. procedure GetJSONValues(W : TJSONSerializer); overload;
  6927. /// return the UTF-8 encoded JSON objects for the values of this TSQLRecord
  6928. // - layout and fields should have been set at TJSONSerializer construction:
  6929. // to append some content to an existing TJsonSerializer, call the
  6930. // AppendAsJsonObject() method
  6931. // - the JSON buffer will be finalized if needed (e.g. non expanded mode),
  6932. // and the supplied TJSONSerializer instance will be freed by this method
  6933. procedure GetJSONValuesAndFree(JSON : TJSONSerializer); overload;
  6934. /// return the UTF-8 encoded JSON objects for the values contained
  6935. // in the current published fields of a TSQLRecord child
  6936. // - only simple fields (i.e. not TSQLRawBlob/TSQLRecordMany) are retrieved:
  6937. // BLOB fields are ignored (use direct access via dedicated methods instead)
  6938. // - if Expand is true, JSON data is an object, for direct use with any Ajax or .NET client:
  6939. // ! {"col1":val11,"col2":"val12"}
  6940. // - if Expand is false, JSON data is serialized (as used in TSQLTableJSON)
  6941. // ! { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] }
  6942. // - if withID is true, then the first ID field value is included
  6943. // - you can customize SQLRecordOptions, e.g. if sftObject/sftBlobDynArray
  6944. // property instance would be serialized as a JSON object or array, not a
  6945. // JSON string (which is the default, as expected by the database storage),
  6946. // or if an "ID_str" string field should be added for JavaScript
  6947. procedure GetJSONValues(JSON: TStream; Expand: boolean; withID: boolean;
  6948. Occasion: TSQLOccasion; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]); overload;
  6949. /// same as overloaded GetJSONValues(), but returning result into a RawUTF8
  6950. // - if UsingStream is not set, it will use a temporary THeapMemoryStream instance
  6951. function GetJSONValues(Expand: boolean; withID: boolean; Occasion: TSQLOccasion;
  6952. UsingStream: TCustomMemoryStream=nil; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]): RawUTF8; overload;
  6953. /// same as overloaded GetJSONValues(), but allowing to set the fields to
  6954. // be retrieved, and returning result into a RawUTF8
  6955. function GetJSONValues(Expand: boolean; withID: boolean;
  6956. const Fields: TSQLFieldBits; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]): RawUTF8; overload;
  6957. /// same as overloaded GetJSONValues(), but allowing to set the fields to
  6958. // be retrieved, and returning result into a RawUTF8
  6959. function GetJSONValues(Expand: boolean; withID: boolean;
  6960. const FieldsCSV: RawUTF8; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]): RawUTF8; overload;
  6961. /// will append the record fields as an expanded JSON object
  6962. // - GetJsonValues() will expect a dedicated TJSONSerializer, whereas this
  6963. // method will add the JSON object directly to any TJSONSerializer
  6964. // - by default, will append the simple fields, unless the Fields optional
  6965. // parameter is customized to a non void value
  6966. procedure AppendAsJsonObject(W: TJSONSerializer; Fields: TSQLFieldBits=[]);
  6967. /// will append all the FillPrepare() records as an expanded JSON array
  6968. // - generates '[{rec1},{rec2},...]' using a loop similar to:
  6969. // ! while FillOne do .. AppendJsonObject() ..
  6970. // - if FieldName is set, the JSON array will be written as a JSON property,
  6971. // i.e. surrounded as '"FieldName":[....],' - note the ',' at the end
  6972. // - by default, will append the simple fields, unless the Fields optional
  6973. // parameter is customized to a non void value
  6974. // - see also TSQLRest.AppendListAsJsonArray for a high-level wrapper method
  6975. procedure AppendFillAsJsonArray(const FieldName: RawUTF8;
  6976. W: TJSONSerializer; Fields: TSQLFieldBits=[]);
  6977. /// change TDocVariantData.Options for all variant published fields
  6978. // - may be used to replace e.g. JSON_OPTIONS_FAST_EXTENDED by JSON_OPTIONS_FAST
  6979. procedure ForceVariantFieldsOptions(aOptions: TDocVariantOptions=JSON_OPTIONS_FAST);
  6980. /// write the field values into the binary buffer
  6981. // - won't write the ID field (should be stored before, with the Count e.g.)
  6982. procedure GetBinaryValues(W: TFileBufferWriter); overload;
  6983. /// write the field values into the binary buffer
  6984. // - won't write the ID field (should be stored before, with the Count e.g.)
  6985. procedure GetBinaryValues(W: TFileBufferWriter; const aFields: TSQLFieldBits); overload;
  6986. /// write the simple field values (excluding ID) into the binary buffer
  6987. procedure GetBinaryValuesSimpleFields(W: TFileBufferWriter);
  6988. /// set the field values from a binary buffer
  6989. // - won't read the ID field (should be read before, with the Count e.g.)
  6990. // - returns true on success, or false in case of invalid content in P^ e.g.
  6991. // - P is updated to the next pending content after the read values
  6992. function SetBinaryValues(var P: PAnsiChar): Boolean;
  6993. /// set the simple field values from a binary buffer
  6994. // - won't read the ID field (should be read before, with the Count e.g.)
  6995. // - returns true on success, or false in case of invalid content in P^ e.g.
  6996. // - P is updated to the next pending content after the read values
  6997. function SetBinaryValuesSimpleFields(var P: PAnsiChar): Boolean;
  6998. /// write the record fields into RawByteString a binary buffer
  6999. // - same as GetBinaryValues(), but also writing the ID field first
  7000. function GetBinary: RawByteString;
  7001. /// set the record fields from a binary buffer saved by GetBinary()
  7002. // - same as SetBinaryValues(), but also reading the ID field first
  7003. function SetBinary(P: PAnsiChar): Boolean;
  7004. /// set all field values from a supplied array of TSQLVar values
  7005. // - Values[] array must match the RecordProps.Field[] order: will return
  7006. // false if the Values[].VType does not match RecordProps.FieldType[]
  7007. function SetFieldSQLVars(const Values: TSQLVarDynArray): boolean;
  7008. /// retrieve a field value from a given property name, as encoded UTF-8 text
  7009. // - you should use strong typing and direct property access, following
  7010. // the ORM approach of the framework; but in some cases (a custom Grid
  7011. // display, for instance), it could be useful to have this method available
  7012. // - will return '' in case of wrong property name
  7013. // - BLOB and dynamic array fields are returned as '\uFFF0base64encodedbinary'
  7014. function GetFieldValue(const PropName: RawUTF8): RawUTF8;
  7015. /// set a field value of a given property name, from some encoded UTF-8 text
  7016. // - you should use strong typing and direct property access, following
  7017. // the ORM approach of the framework; but in some cases (a custom Grid
  7018. // display, for instance), it could be useful to have this method available
  7019. // - won't do anything in case of wrong property name
  7020. // - expect BLOB and dynamic array fields encoded as SQlite3 BLOB literals
  7021. // ("x'01234'" e.g.) or '\uFFF0base64encodedbinary'
  7022. procedure SetFieldValue(const PropName: RawUTF8; Value: PUTF8Char);
  7023. {$ifndef NOVARIANTS}
  7024. /// retrieve the record content as a TDocVariant custom variant object
  7025. function GetAsDocVariant(withID: boolean; const withFields: TSQLFieldBits;
  7026. options: PDocVariantOptions=nil): variant; overload;
  7027. {$ifdef HASINLINE}inline;{$endif}
  7028. /// retrieve the record content as a TDocVariant custom variant object
  7029. procedure GetAsDocVariant(withID: boolean; const withFields: TSQLFieldBits;
  7030. var result: variant; options: PDocVariantOptions=nil); overload;
  7031. /// retrieve the simple record content as a TDocVariant custom variant object
  7032. function GetSimpleFieldsAsDocVariant(withID: boolean=true;
  7033. options: PDocVariantOptions=nil): variant;
  7034. /// retrieve the published property value into a Variant
  7035. // - will set the Variant type to the best matching kind according to the
  7036. // property type
  7037. // - will return a null variant in case of wrong property name
  7038. // - BLOB fields are returned as SQlite3 BLOB literals ("x'01234'" e.g.)
  7039. // - dynamic array fields are returned as a Variant array
  7040. function GetFieldVariant(const PropName: string): Variant;
  7041. /// set the published property value from a Variant value
  7042. // - will convert from the variant type into UTF-8 text before setting the
  7043. // value (so will work with any kind of Variant)
  7044. // - won't do anything in case of wrong property name
  7045. // - expect BLOB fields encoded as SQlite3 BLOB literals ("x'01234'" e.g.)
  7046. procedure SetFieldVariant(const PropName: string; const Source: Variant);
  7047. {$endif}
  7048. /// prepare to get values from a TSQLTable result
  7049. // - then call FillRow(1..Table.RowCount) to get any row value
  7050. // - or you can also loop through all rows with
  7051. // ! while Rec.FillOne do
  7052. // ! dosomethingwith(Rec);
  7053. // - the specified TSQLTable is stored in an internal fTable protected field
  7054. // - set aCheckTableName if you want e.g. the Field Names from the Table
  7055. // any pending 'TableName.' trimmed before matching to the current record
  7056. procedure FillPrepare(Table: TSQLTable; aCheckTableName: TSQLCheckTableName=ctnNoCheck); overload;
  7057. /// prepare to get values from a SQL where statement
  7058. // - returns true in case of success, false in case of an error during SQL request
  7059. // - then call FillRow(1..Table.RowCount) to get any row value
  7060. // - or you can also loop through all rows with
  7061. // ! while Rec.FillOne do
  7062. // ! dosomethingwith(Rec);
  7063. // - a temporary TSQLTable is created then stored in an internal fTable protected field
  7064. // - if aSQLWhere is left to '', all rows are retrieved as fast as possible
  7065. // (e.g. by-passing SQLite3 virtual table modules for external databases)
  7066. // - the WHERE clause should use inlined parameters (like 'Name=:('Arnaud'):')
  7067. // for better server speed - note that you can use FormatUTF8() as such:
  7068. // ! aRec.FillPrepare(Client,FormatUTF8('Salary>? AND Salary<?',[],[1000,2000]));
  7069. // or call the overloaded FillPrepare() method directly with BoundsSQLWhere
  7070. // array of parameters
  7071. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  7072. // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
  7073. // you may need to access only one or several fields, and will save remote
  7074. // bandwidth by specifying the needed fields
  7075. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  7076. // - note that you should not use this aCustomFieldsCSV optional parameter if
  7077. // you want to Update the retrieved record content later, since any
  7078. // missing fields will be left with previous values - but BatchUpdate() can be
  7079. // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
  7080. function FillPrepare(aClient: TSQLRest; const aSQLWhere: RawUTF8='';
  7081. const aCustomFieldsCSV: RawUTF8=''; aCheckTableName: TSQLCheckTableName=ctnNoCheck): boolean; overload;
  7082. /// prepare to get values using a specified WHERE clause with '%' parameters
  7083. // - returns true in case of success, false in case of an error during SQL request
  7084. // - then call FillRow(1..Table.RowCount) to get any row value
  7085. // - or you can also loop through all rows with
  7086. // ! while Rec.FillOne do
  7087. // ! dosomethingwith(Rec);
  7088. // - a temporary TSQLTable is created then stored in an internal fTable protected field
  7089. // - for better server speed, the WHERE clause should use bound parameters
  7090. // identified as '?' in the FormatSQLWhere statement, which is expected to
  7091. // follow the order of values supplied in BoundsSQLWhere open array - use
  7092. // DateToSQL/DateTimeToSQL for TDateTime, or directly any integer / double /
  7093. // currency / RawUTF8 values to be bound to the request as parameters
  7094. // - note that this method prototype changed with revision 1.17 of the
  7095. // framework: array of const used to be ParamsSQLWhere and '%' in the
  7096. // FormatSQLWhere statement, whereas it now expects bound parameters as '?'
  7097. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  7098. // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
  7099. // you may need to access only one or several fields, and will save remote
  7100. // bandwidth by specifying the needed fields
  7101. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  7102. // - note that you should not use this aCustomFieldsCSV optional parameter if
  7103. // you want to Update the retrieved record content later, since any
  7104. // missing fields will be left with previous values - but BatchUpdate() can be
  7105. // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
  7106. function FillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  7107. const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): boolean; overload;
  7108. /// prepare to get values using a specified WHERE clause with '%' and '?' parameters
  7109. // - returns true in case of success, false in case of an error during SQL request
  7110. // - then call FillRow(1..Table.RowCount) to get any row value
  7111. // - or you can also loop through all rows with
  7112. // ! while Rec.FillOne do
  7113. // ! dosomethingwith(Rec);
  7114. // - a temporary TSQLTable is created then stored in an internal fTable
  7115. // protected field
  7116. // - the FormatSQLWhere clause will replace all '%' chars with the supplied
  7117. // ParamsSQLWhere[] supplied values, and bind all '?' chars as bound
  7118. // parameters with BoundsSQLWhere[] values
  7119. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  7120. // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
  7121. // you may need to access only one or several fields, and will save remote
  7122. // bandwidth by specifying the needed fields
  7123. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  7124. // - note that you should not use this aCustomFieldsCSV optional parameter if
  7125. // you want to Update the retrieved record content later, since any
  7126. // missing fields will be left with previous values - but BatchUpdate() can be
  7127. // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
  7128. function FillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  7129. const ParamsSQLWhere, BoundsSQLWhere: array of const;
  7130. const aCustomFieldsCSV: RawUTF8=''): boolean; overload;
  7131. /// prepare to get values from a list of IDs
  7132. // - returns true in case of success, false in case of an error during SQL request
  7133. // - then call FillRow(1..Table.RowCount) to get any row value
  7134. // - or you can also loop through all rows with
  7135. // ! while Rec.FillOne do
  7136. // ! dosomethingwith(Rec);
  7137. // - a temporary TSQLTable is created then stored in an internal fTable protected field
  7138. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  7139. // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
  7140. // you may need to access only one or several fields, and will save remote
  7141. // bandwidth by specifying the needed fields
  7142. // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
  7143. // - note that you should not use this aCustomFieldsCSV optional parameter if
  7144. // you want to Update the retrieved record content later, since any
  7145. // missing fields will be left with previous values - but BatchUpdate() can be
  7146. // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
  7147. function FillPrepare(aClient: TSQLRest; const aIDs: array of Int64;
  7148. const aCustomFieldsCSV: RawUTF8=''): boolean; overload;
  7149. // / prepare to loop through a JOINed statement including TSQLRecordMany fields
  7150. // - all TSQLRecordMany.Dest published fields will now contain a true TSQLRecord
  7151. // instance, ready to be filled with the JOINed statement results (these
  7152. // instances will be released at FillClose) - the same for Source which will
  7153. // point to the self instance
  7154. // - the aFormatSQLJoin clause will define a WHERE clause for an automated
  7155. // JOINed statement, including TSQLRecordMany published properties (and
  7156. // their nested properties)
  7157. // - returns true in case of success, false in case of an error during SQL request
  7158. // - a typical use could be the following:
  7159. // ! if aProd.FillPrepareMany(Database,
  7160. // ! 'Owner=? and Categories.Dest.Name=? and (Sizes.Dest.Name=? or Sizes.Dest.Name=?)',[],
  7161. // ! ['mark','for boy','small','medium']) then
  7162. // ! while aProd.FillOne do
  7163. // ! // here e.g. aProd.Categories.Dest are instantied (and Categories.Source=aProd)
  7164. // ! writeln(aProd.Name,' ',aProd.Owner,' ',aProd.Categories.Dest.Name,' ',aProd.Sizes.Dest.Name);
  7165. // ! // you may also use aProd.FillTable to fill a grid, e.g.
  7166. // ! // (do not forget to set aProd.FillTable.OwnerMustFree := false)
  7167. // this will execute a JOINed SELECT statement similar to the following:
  7168. // $ select p.*, c.*, s.*
  7169. // $ from Product p, Category c, Categories cc, Size s, Sizes ss
  7170. // $ where c.id=cc.dest and cc.source=p.id and
  7171. // $ s.id=ss.dest and ss.source=p.id and
  7172. // $ p.Owner='mark' and c.Name='for boy' and (s.Name='small' or s.Name='medium')
  7173. // - the FormatSQLWhere clause will replace all '%' chars with the supplied
  7174. // ParamsSQLWhere[] supplied values, and bind all '?' chars as parameters
  7175. // with BoundsSQLWhere[] values
  7176. // - you SHALL call explicitely the FillClose method before using any
  7177. // methods of nested TSQLRecordMany instances which may override the Dest
  7178. // instance content (e.g. ManySelect) to avoid any GPF
  7179. // - is used by TSQLRecord.CreateAndFillPrepareMany constructor
  7180. function FillPrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8;
  7181. const aParamsSQLJoin, aBoundsSQLJoin: array of const): boolean;
  7182. /// compute a JOINed statement including TSQLRecordMany fields
  7183. // - is called by FillPrepareMany() to retrieve the JSON of the corresponding
  7184. // request: so you could use this method to retrieve directly the same
  7185. // information, ready to be transmitted (e.g. as RawJSON) to a client
  7186. function EnginePrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8;
  7187. const aParamsSQLJoin, aBoundsSQLJoin: array of const;
  7188. out ObjectsClass: TSQLRecordClassDynArray; out SQL: RawUTF8): RawUTF8;
  7189. /// fill all published properties of an object from a TSQLTable prepared row
  7190. // - FillPrepare() must have been called before
  7191. // - if Dest is nil, this object values are filled
  7192. // - if Dest is not nil, this object values will be filled, but it won't
  7193. // work with TSQLRecordMany properties (i.e. after FillPrepareMany call)
  7194. // - ID field is updated if first Field Name is 'ID'
  7195. // - Row number is from 1 to Table.RowCount
  7196. // - setter method (write Set*) is called if available
  7197. // - handle UTF-8 SQL to Delphi values conversion (see TPropInfo mapping)
  7198. // - this method has been made virtual e.g. so that a calculated value can be
  7199. // used in a custom field
  7200. function FillRow(aRow: integer; aDest: TSQLRecord=nil): boolean; virtual;
  7201. /// fill all published properties of this object from the next available
  7202. // TSQLTable prepared row
  7203. // - FillPrepare() must have been called before
  7204. // - the Row number is taken from property FillCurrentRow
  7205. // - return true on success, false if no more Row data is available
  7206. // - internally call FillRow() to update published properties values
  7207. function FillOne: boolean;
  7208. /// go to the first prepared row, ready to loop through all rows with FillOne()
  7209. // - the Row number (property FillCurrentRow) is reset to 1
  7210. // - return true on success, false if no Row data is available
  7211. // - you can use it e.g. as:
  7212. // ! while Rec.FillOne do
  7213. // ! dosomethingwith(Rec);
  7214. // ! if Rec.FillRewind then
  7215. // ! while Rec.FillOne do
  7216. // ! dosomeotherthingwith(Rec);
  7217. function FillRewind: boolean;
  7218. /// close any previous FillPrepare..FillOne loop
  7219. // - is called implicitely by FillPrepare() call to release any previous loop
  7220. // - release the internal hidden TSQLTable instance if necessary
  7221. // - is not mandatory if the TSQLRecord is released just after, since
  7222. // TSQLRecord.Destroy will call it
  7223. // - used e.g. by FillFrom methods below to avoid any GPF/memory confusion
  7224. procedure FillClose;
  7225. /// will iterate over all FillPrepare items, appending them as a JSON array
  7226. // - creates a JSON array of all record rows, using
  7227. // ! while FillOne do GetJSONValues(W)...
  7228. procedure AppendFillAsJsonValues(W: TJSONSerializer);
  7229. /// fill all published properties of this object from a TSQLTable result row
  7230. // - call FillPrepare() then FillRow(Row)
  7231. procedure FillFrom(Table: TSQLTable; Row: integer); overload;
  7232. /// fill all published properties of this object from a JSON result row
  7233. // - create a TSQLTable from the JSON data
  7234. // - call FillPrepare() then FillRow(Row)
  7235. procedure FillFrom(const JSONTable: RawUTF8; Row: integer); overload;
  7236. /// fill all published properties of this object from a JSON object result
  7237. // - use JSON data, as exported by GetJSONValues()
  7238. // - JSON data may be expanded or not
  7239. // - make an internal copy of the JSONTable RawUTF8 before calling
  7240. // FillFrom() below
  7241. // - if FieldBits is defined, it will store the identified field index
  7242. procedure FillFrom(const JSONRecord: RawUTF8; FieldBits: PSQLFieldBits=nil); overload;
  7243. /// fill all published properties of this object from a JSON result
  7244. // - the data inside P^ is modified (unescaped and transformed): don't call
  7245. // FillFrom(pointer(JSONRecordUTF8)) but FillFrom(JSONRecordUTF8) which makes
  7246. // a temporary copy of the JSONRecordUTF8 text
  7247. // - use JSON data, as exported by GetJSONValues()
  7248. // - JSON data may be expanded or not
  7249. // - if FieldBits is defined, it will store the identified field index
  7250. procedure FillFrom(P: PUTF8Char; FieldBits: PSQLFieldBits=nil); overload;
  7251. /// fill all published properties of this object from another object
  7252. // - source object must be a parent or of the same class as the current record
  7253. // - copy all COPIABLE_FIELDS, i.e. all fields excluding tftMany (because
  7254. // those fields don't contain any data, but a TSQLRecordMany instance
  7255. // which allow to access to the pivot table data)
  7256. procedure FillFrom(aRecord: TSQLRecord); overload;
  7257. /// fill the specified properties of this object from another object
  7258. // - source object must be a parent or of the same class as the current record
  7259. // - copy the fields, as specified by their bit index in the source record
  7260. procedure FillFrom(aRecord: TSQLRecord; const aRecordFieldBits: TSQLFieldBits); overload;
  7261. {$ifndef NOVARIANTS}
  7262. /// fill all published properties of this object from a supplied TDocVariant
  7263. // object document
  7264. // - is a wrapper around VariantSaveJSON() + FillFrom() methods
  7265. procedure FillFrom(const aDocVariant: variant); overload;
  7266. {$endif}
  7267. /// fill a published property value of this object from a UTF-8 encoded value
  7268. // - see TPropInfo about proper Delphi / UTF-8 type mapping/conversion
  7269. // - use this method to fill a BLOB property, i.e. a property defined with
  7270. // type TSQLRawBlob, since by default all BLOB properties are not
  7271. // set by the standard Retrieve() method (to save bandwidth)
  7272. // - if FieldBits is defined, it will store the identified field index
  7273. procedure FillValue(PropName, Value: PUTF8Char; wasString: boolean;
  7274. FieldBits: PSQLFieldBits=nil);
  7275. /// return true if all published properties values in Other are identical to
  7276. // the published properties of this object
  7277. // - work with different classes: Reference properties name must just be
  7278. // present in the calling object
  7279. // - only simple fields (i.e. not TSQLRawBlob/TSQLRecordMany) are compared
  7280. // - compare the text representation of the values: fields may be of different
  7281. // type, encoding or precision, but still have same values
  7282. function SameValues(Reference: TSQLRecord): boolean;
  7283. /// return true if all published properties values in Other are identical to
  7284. // the published properties of this object
  7285. // - instances must be of the same class type
  7286. // - only simple fields (i.e. not TSQLRawBlob/TSQLRecordMany) are compared
  7287. // - comparaison is much faster than SameValues() above
  7288. function SameRecord(Reference: TSQLRecord): boolean;
  7289. /// clear the values of all published properties, and also the ID property
  7290. procedure ClearProperties; overload;
  7291. /// clear the values of specified published properties
  7292. // - '' would leave the content untouched, '*' will clear all simple fields
  7293. procedure ClearProperties(const aFieldsCSV: RawUTF8); overload;
  7294. /// set the simple fields with the supplied values
  7295. // - the aSimpleFields parameters must follow explicitely the order of published
  7296. // properties of the supplied aTable class, excepting the TSQLRawBlob and
  7297. // TSQLRecordMany kind (i.e. only so called "simple fields") - in particular,
  7298. // parent properties must appear first in the list
  7299. // - the aSimpleFields must have exactly the same count of parameters as there are
  7300. // "simple fields" in the published properties
  7301. // - return true on success, but be aware that the field list must match
  7302. // the field layout, otherwise if may return true but will corrupt data
  7303. function SimplePropertiesFill(const aSimpleFields: array of const): boolean;
  7304. /// initialize a TDynArray wrapper to map dynamic array property values
  7305. // - if the field name is not existing or not a dynamic array, result.IsVoid
  7306. // will be TRUE
  7307. function DynArray(const DynArrayFieldName: RawUTF8): TDynArray; overload;
  7308. /// initialize a TDynArray wrapper to map dynamic array property values
  7309. // - this overloaded version expect the dynamic array to have been defined
  7310. // with a not null index attribute, e.g.
  7311. // ! published
  7312. // ! property Ints: TIntegerDynArray index 1 read fInts write fInts;
  7313. // ! property Currency: TCurrencyDynArray index 2 read fCurrency write fCurrency;
  7314. // - if the field index is not existing or not a dynamic array, result.IsVoid
  7315. // will be TRUE
  7316. function DynArray(DynArrayFieldIndex: integer): TDynArray; overload;
  7317. /// this property stores the record's integer ID
  7318. // - if this TSQLRecord is not a instance, but a field value in a published
  7319. // property of type sftID (i.e. TSQLRecord(aID)), this method will try
  7320. // to retrieve it; but prefered method is to typecast it via PtrInt(aProperty),
  7321. // because GetID() relies on some low-level Windows memory mapping trick, and
  7322. // will recognize an ID value up to 1,048,576 (i.e. $100000)
  7323. // - notice: the Setter should not be used usualy; you should not have to write
  7324. // aRecord.ID := someID in your code, since the ID is set during Retrieve or
  7325. // Add of the record
  7326. property ID: TID read GetID;
  7327. /// this property gives direct access to the record's integer ID
  7328. // - using IDValue expects this TSQLRecord to be a true instance, not a
  7329. // transtyped sftID (i.e. TSQLRecord(aID))
  7330. property IDValue: TID read fID write fID;
  7331. /// this read-only property can be used to retrieve the ID as a TSQLRecord object
  7332. // - published properties of type TSQLRecord (one-to-many relationship) do not
  7333. // store real class instances (only exception is if they inherit from
  7334. // TSQLRecordMany) - you can use this value to assign a TSQLRecord instance
  7335. // to a published property, as such:
  7336. // ! Main := TSQLRecordMain.Create;
  7337. // ! Client.Add(Main);
  7338. // ! Detail := TSQLRecordDetail.Create;
  7339. // ! Detail.Main := Main.AsTSQLRecord; // will store Main.ID in MAIN column
  7340. // ! Client.Add(Detail);
  7341. // - is especially useful on 64-bit plaform, since on 32 bit:
  7342. // ! Detail.Main := pointer(Main.ID)
  7343. // compiles (whereas it won't on 64-bit) and is the same than platform-independent
  7344. // ! Detail.Main := Main.AsTSQLRecord;
  7345. // - using Main.AsTSQLRecord will ensure that the ID is retrieved, even
  7346. // if Main itself is not a true instance
  7347. // - if the stored ID is bigger than 32 bits, then it would raise an
  7348. // EORMException: in this case, you should use a TID / T*ID kind of
  7349. // published property, and not a TSQLRecord, which is limited to the
  7350. // pointer size
  7351. property AsTSQLRecord: pointer read GetIDAsPointer;
  7352. /// this property is set to true, if any published property is a BLOB (TSQLRawBlob)
  7353. property HasBlob: boolean read GetHasBlob;
  7354. /// this property returns the published property count with any valid
  7355. // database field except TSQLRawBlob/TSQLRecordMany
  7356. // - by default, the TSQLRawBlob (BLOB) fields are not included into this set:
  7357. // they must be read specificaly (in order to spare bandwidth)
  7358. // - TSQLRecordMany fields are not accessible directly, but as instances
  7359. // created by TSQLRecord.Create
  7360. property SimpleFieldCount: integer read GetSimpleFieldCount;
  7361. /// this property contains the TSQLTable after a call to FillPrepare()
  7362. property FillTable: TSQLTable read GetTable;
  7363. /// this property contains the current row number (beginning with 1),
  7364. // initialized to 1 by FillPrepare(), which will be read by FillOne
  7365. property FillCurrentRow: integer read GetFillCurrentRow;
  7366. /// used internally by FillPrepare() and corresponding Fill*() methods
  7367. property FillContext: TSQLRecordFill read fFill;
  7368. /// this property contains the internal state counter of the server database
  7369. // when the data was retrieved from it
  7370. // - can be used to check if retrieved data may be out of date
  7371. property InternalState: cardinal read fInternalState;
  7372. published
  7373. { published properties in inherited classes will be interpreted as SQL fields }
  7374. end;
  7375. /// allow on-the-fly translation of a TSQLTable grid value
  7376. // - should return valid JSON value of the given cell (i.e. quoted strings,
  7377. // or valid JSON object/array)
  7378. // - e.g. TSQLTable.OnExportValue property will customize TSQLTable's
  7379. // GetJSONValues, GetHtmlTable, and GetCSVValues methods returned content
  7380. TOnSQLTableGetValue = function(Sender: TSQLTable; Row, Field: integer): RawJSON of object;
  7381. /// wrapper to an ORM result table, staticaly stored as UTF-8 text
  7382. // - contain all result in memory, until destroyed
  7383. // - first row contains the field names
  7384. // - following rows contains the data itself
  7385. // - GetString() can be used in a TDrawString
  7386. // - will be implemented as TSQLTableJSON for remote access through optimized
  7387. // JSON content
  7388. TSQLTable = class
  7389. protected
  7390. fRowCount: integer;
  7391. fFieldCount: integer;
  7392. /// contains the data, as returned by sqlite3_get_table()
  7393. fResults: PPUTF8CharArray;
  7394. /// contains the TSQLFieldType and TypeInfo(enumerate), after calculation
  7395. // from the fQueryTables values
  7396. fFieldType: array of record
  7397. // the field kind, as in JSON (match TSQLPropInfo.SQLFieldTypeStored)
  7398. ContentType: TSQLFieldType;
  7399. // the field size in bytes; -1 means not computed yet
  7400. ContentSize: integer;
  7401. // used for sftEnumerate, sftSet and sftBlobDynArray fields
  7402. ContentTypeInfo: pointer;
  7403. // the corresponding index in fQueryTables[]
  7404. TableIndex: integer;
  7405. end;
  7406. fFieldTypeAllRows: boolean;
  7407. /// the field names
  7408. fFieldNames: TRawUTF8DynArray;
  7409. /// used by FieldIndex() for fast binary search
  7410. fFieldNameOrder: TCardinalDynArray;
  7411. /// contain the fResults[] pointers, after a IDColumnHide() call
  7412. fIDColumn, fNotIDColumn: array of PUTF8Char;
  7413. /// index of a 'ID' field, -1 if none (e.g. after IDColumnHide method call)
  7414. fFieldIndexID: integer;
  7415. /// the internal state counter of the database when the data was retrieved
  7416. fInternalState: cardinal;
  7417. /// contains the parameters used for sorting
  7418. fSortParams: TSQLTableSortParams;
  7419. /// contains the TSQLRecord instances created by NewRecord method
  7420. fOwnedRecords: TObjectList;
  7421. /// if the TSQLRecord is the owner of this table, i.e. if it must free it
  7422. fOwnerMustFree: Boolean;
  7423. /// current cursor row (1..RowCount), as set by the Step() method
  7424. fStepRow: integer;
  7425. /// information about the Query sourcing this result set
  7426. fQueryTables: TSQLRecordClassDynArray;
  7427. fQueryColumnTypes: array of TSQLFieldType;
  7428. fQuerySQL: RawUTF8;
  7429. fQueryTableNameFromSQL: RawUTF8;
  7430. fQueryTableIndexFromSQL: integer; // -2=nosearch -1=notfound fQueryTables[0..n]
  7431. /// field length information
  7432. fFieldLengthMean: TIntegerDynArray;
  7433. fFieldLengthMeanSum: integer;
  7434. /// column bit set at parsing to mark a string value (e.g. "..." in JSON)
  7435. fFieldParsedAsString: set of 0..255;
  7436. fOnExportValue: TOnSQLTableGetValue;
  7437. /// avoid GPF when TSQLTable is nil
  7438. function GetRowCount: integer; {$ifdef HASINLINE}inline;{$endif}
  7439. /// fill the fFieldType[] array (from fQueryTables[] or fResults[] content)
  7440. procedure InitFieldTypes;
  7441. /// fill the internal fFieldNames[] array
  7442. procedure InitFieldNames;
  7443. /// guess the property type information from ORM
  7444. function FieldPropFromTables(const PropName: RawUTF8;
  7445. out PropInfo: TSQLPropInfo; out TableIndex: integer): TSQLFieldType;
  7446. function GetQueryTableNameFromSQL: RawUTF8;
  7447. public
  7448. /// initialize the result table
  7449. // - you can optionaly associate the corresponding TSQLRecordClass types,
  7450. // by which the results were computed (it will use RTTI for column typing)
  7451. constructor Create(const aSQL: RawUTF8);
  7452. /// initialize the result table
  7453. // - you can associate the corresponding TSQLRecordClass types,
  7454. // by which the results were computed (it will use RTTI for column typing)
  7455. constructor CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL: RawUTF8);
  7456. /// initialize the result table
  7457. // - you can set the expected column types matching the results column layout
  7458. constructor CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType; const aSQL: RawUTF8);
  7459. /// free associated memory and owned records
  7460. destructor Destroy; override;
  7461. /// read-only access to a particular field value, as UTF-8 encoded buffer
  7462. // - if Row and Fields are correct, returns a pointer to the UTF-8 buffer,
  7463. // or nil if the corresponding JSON was null or ""
  7464. // - if Row and Fields are not correct, returns nil
  7465. function Get(Row,Field: integer): PUTF8Char; overload;
  7466. {$ifdef HASINLINE}inline;{$endif}
  7467. /// read-only access to a particular field value, as RawUTF8 text
  7468. function GetU(Row,Field: integer): RawUTF8; overload;
  7469. /// read-only access to a particular field value, as UTF-8 encoded buffer
  7470. // - points to memory buffer allocated by Init()
  7471. function Get(Row: integer; const FieldName: RawUTF8): PUTF8Char; overload;
  7472. /// read-only access to a particular field value, as RawUTF8 text
  7473. function GetU(Row: integer; const FieldName: RawUTF8): RawUTF8; overload;
  7474. /// read-only access to a particular field value, as Win Ansi text
  7475. function GetA(Row,Field: integer): WinAnsiString;
  7476. /// read-only access to a particular field value, as Win Ansi text shortstring
  7477. function GetS(Row,Field: integer): shortstring;
  7478. {$ifndef NOVARIANTS}
  7479. /// read-only access to a particular field value, as a Variant
  7480. // - text will be stored as RawUTF8 (as varString type)
  7481. // - will try to use the most approriate Variant type for conversion (will
  7482. // use e.g. TDateTime for sftDateTime, or a TDocVariant for JSON objects
  7483. // in a sftVariant column) - so you should better set the exact field types
  7484. // (e.g. from ORM) before calling this method
  7485. function GetVariant(Row,Field: integer; Client: TObject): variant; overload;
  7486. /// read-only access to a particular field value, as a Variant
  7487. // - text will be stored as RawUTF8 (as varString type)
  7488. // - will try to use the most approriate Variant type for conversion (will
  7489. // use e.g. TDateTime for sftDateTime, or a TDocVariant for JSON objects
  7490. // in a sftVariant column) - so you should better set the exact field types
  7491. // (e.g. from ORM) before calling this method
  7492. procedure GetVariant(Row,Field: integer; Client: TObject; var result: variant); overload;
  7493. /// read-only access to a particular field, via a lookup field name
  7494. // - will call GetVariant() on the corresponding field
  7495. // - returns null if the lookup did not have any match
  7496. function GetValue(const aLookupFieldName,aLookupValue,aValueFieldName: RawUTF8): variant;
  7497. {$endif}
  7498. /// read-only access to a particular field value, as VCL string text
  7499. // - the global UTF8ToString() function will be used for the conversion:
  7500. // for proper i18n handling before Delphi 2009, you should use the
  7501. // overloaded method with aUTF8ToString=Language.UTF8ToString
  7502. function GetString(Row,Field: integer): string;
  7503. /// read-only access to a particular field value, as fast Unicode string text
  7504. // - SynUnicode is either WideString, either UnicodeString, depending on the
  7505. // Delphi compiler revision, to ensure fastest native Unicode process available
  7506. function GetSynUnicode(Row,Field: integer): SynUnicode;
  7507. /// fill a unicode buffer with a particular field value
  7508. // - return number of wide characters written in Dest^
  7509. function GetWP(Row,Field: integer; Dest: PWideChar; MaxDestChars: cardinal): integer;
  7510. /// read-only access to a particular field value, as UTF-16 Unicode text
  7511. // - Raw Unicode is WideChar(zero) terminated
  7512. // - its content is allocated to contain all WideChars (not trimed to 255,
  7513. // like GetWP() above
  7514. function GetW(Row,Field: integer): RawUnicode;
  7515. /// read-only access to a particular field value, as integer value
  7516. function GetAsInteger(Row,Field: integer): integer; overload;
  7517. {$ifdef HASINLINE}inline;{$endif}
  7518. /// read-only access to a particular field value, as integer value
  7519. function GetAsInteger(Row: integer; const FieldName: RawUTF8): integer; overload;
  7520. {$ifdef HASINLINE}inline;{$endif}
  7521. /// read-only access to a particular field value, as Int64 value
  7522. function GetAsInt64(Row,Field: integer): Int64; overload;
  7523. {$ifdef HASINLINE}inline;{$endif}
  7524. /// read-only access to a particular field value, as Int64 value
  7525. function GetAsInt64(Row: integer; const FieldName: RawUTF8): Int64; overload;
  7526. {$ifdef HASINLINE}inline;{$endif}
  7527. /// read-only access to a particular field value, as extended value
  7528. function GetAsFloat(Row,Field: integer): TSynExtended; overload;
  7529. {$ifdef HASINLINE}inline;{$endif}
  7530. /// read-only access to a particular field value, as extended value
  7531. function GetAsFloat(Row: integer; const FieldName: RawUTF8): TSynExtended; overload;
  7532. {$ifdef HASINLINE}inline;{$endif}
  7533. /// read-only access to a particular field value, as TDateTime value
  7534. // - explicit sftDateTime will be converted from ISO-8601 text
  7535. // - sftTimeLog, sftModTime, sftCreateTime will expect the content to be
  7536. // encoded as a TTimeLog Int64 value - as sftInteger may have been
  7537. // identified by TSQLTable.InitFieldTypes
  7538. // - for sftTimeLog, sftModTime, sftCreateTime fields, you may have to force
  7539. // the column type, since it may be identified as sftInteger or sftCurrency
  7540. // by default from its JSON number content, e.g. via:
  7541. // ! aTable.SetFieldType('FieldName',sftModTime);
  7542. // - sftCurrency,sftFloat will return the corresponding double value
  7543. // - any other types will try to convert ISO-8601 text }
  7544. function GetAsDateTime(Row,Field: integer): TDateTime; overload;
  7545. /// read-only access to a particular field value, as TDateTime value
  7546. function GetAsDateTime(Row: integer; const FieldName: RawUTF8): TDateTime; overload;
  7547. /// read-only access to a particular field value, as currency value
  7548. function GetAsCurrency(Row,Field: integer): currency; overload;
  7549. {$ifdef HASINLINE}inline;{$endif}
  7550. /// read-only access to a particular field value, as currency value
  7551. function GetAsCurrency(Row: integer; const FieldName: RawUTF8): currency; overload;
  7552. {$ifdef HASINLINE}inline;{$endif}
  7553. /// read-only access to a particular field value, ready to be displayed
  7554. // - mostly used with Row=0, i.e. to get a display value from a field name
  7555. // - use "string" type, i.e. UnicodeString for Delphi 2009+
  7556. // - value is first un-camel-cased: 'OnLine' value will return 'On line' e.g.
  7557. // - then System.LoadResStringTranslate() is called if available
  7558. function GetCaption(Row,Field: integer): string;
  7559. /// read-only access to a particular Blob value
  7560. // - a new TSQLRawBlob is created
  7561. // - Blob data is converted from SQLite3 BLOB literals (X'53514C697465' e.g.)
  7562. // or Base-64 encoded content ('\uFFF0base64encodedbinary')
  7563. // - prefered manner is to directly use REST protocol to retrieve a blob field
  7564. function GetBlob(Row,Field: integer): TSQLRawBlob;
  7565. /// read-only access to a particular Blob value
  7566. // - a new TBytes is created
  7567. // - Blob data is converted from SQLite3 BLOB literals (X'53514C697465' e.g.)
  7568. // or Base-64 encoded content ('\uFFF0base64encodedbinary')
  7569. // - prefered manner is to directly use REST protocol to retrieve a blob field
  7570. function GetBytes(Row,Field: integer): TBytes;
  7571. /// read-only access to a particular Blob value
  7572. // - a new TCustomMemoryStream is created - caller shall free its instance
  7573. // - Blob data is converted from SQLite3 BLOB literals (X'53514C697465' e.g.)
  7574. // or Base-64 encoded content ('\uFFF0base64encodedbinary')
  7575. // - prefered manner is to directly use REST protocol to retrieve a blob field
  7576. function GetStream(Row,Field: integer): TStream;
  7577. /// read-only access to a particular field value, as VCL text
  7578. // - Client is one TSQLClient instance (used to display TRecordReference via
  7579. // the associated TSQLModel)
  7580. // - returns the Field Type
  7581. // - return generic string Text, i.e. UnicodeString for Delphi 2009+,
  7582. // ready to be displayed to the VCL, for sftEnumerate, sftTimeLog
  7583. // and sftRecord/sftRecordVersion/sftID/sftTID
  7584. // - returns '' as string Text, if text can by displayed directly
  7585. // with Get*() methods above
  7586. // - returns '' for other properties kind, if UTF8ToString is nil,
  7587. // or the ready to be displayed value if UTF8ToString event is set
  7588. // (to be used mostly with Language.UTF8ToString)
  7589. // - CustomFormat can optionaly set a custom format string, e.g. '%f' or '%n'
  7590. // or complex FormatFloat()/FormatCurr() syntax (as '#,##0.00') for sftFloat
  7591. // and sftCurrency columns (instead of plain JSON float value), or
  7592. // date/time format as expected by FormatDateTime() for all date time kind
  7593. // of fields (as sftDateTime, sftTimeLog, sftModTime, sftCreateTime)
  7594. function ExpandAsString(Row,Field: integer; Client: TObject; out Text: string;
  7595. const CustomFormat: string=''): TSQLFieldType;
  7596. /// read-only access to a particular field value, as VCL text
  7597. // - this method is just a wrapper around ExpandAsString method, returning
  7598. // the content as a SynUnicode string type (i.e. UnicodeString since Delphi
  7599. // 2009, and WideString for non Unicode versions of Delphi)
  7600. // - it is used by the reporting layers of the framework (e.g. TSQLRibbon.AddToReport)
  7601. function ExpandAsSynUnicode(Row,Field: integer; Client: TObject; out Text: SynUnicode): TSQLFieldType;
  7602. /// read-only access to a particular DateTime field value
  7603. // - expect SQLite3 TEXT field in ISO 8601 'YYYYMMDD hhmmss' or
  7604. // 'YYYY-MM-DD hh:mm:ss' format
  7605. function GetDateTime(Row,Field: integer): TDateTime;
  7606. {$ifdef PUREPASCAL} {$ifdef HASINLINE}inline;{$endif} {$endif}
  7607. /// read-only access to a particular TTimeLog field value
  7608. // - return the result as TTimeLogBits.Text() Iso-8601 encoded text
  7609. function GetTimeLog(Row,Field: integer; Expanded: boolean; FirstTimeChar: AnsiChar = 'T'): RawUTF8;
  7610. /// widechar length (UTF-8 decoded as UTF-16) of a particular field value
  7611. // - could be used with VCL's UnicodeString, or for Windows API
  7612. function LengthW(Row,Field: integer): integer;
  7613. /// get all values for a specified field into a dynamic RawUTF8 array
  7614. // - don't perform any conversion, but just create an array of raw PUTF8Char data
  7615. procedure GetRowValues(Field: integer; out Values: TRawUTF8DynArray); overload;
  7616. /// get all values for a specified field into a dynamic Integer array
  7617. procedure GetRowValues(Field: integer; out Values: TInt64DynArray); overload;
  7618. /// get all values for a specified field as CSV
  7619. // - don't perform any conversion, but create a CSV from raw PUTF8Char data
  7620. function GetRowValues(Field: integer; Sep: AnsiChar=','): RawUTF8; overload;
  7621. {$ifndef NOVARIANTS}
  7622. /// retrieve a field value in a variant
  7623. // - returns null if the row/field is incorrect
  7624. // - expand* methods would allow to return human-friendly representations
  7625. procedure GetAsVariant(row,field: integer; out value: variant;
  7626. expandTimeLogAsText,expandEnumsAsText,expandHugeIDAsUniqueIdentifier: boolean;
  7627. options: TDocVariantOptions=JSON_OPTIONS_FAST);
  7628. /// retrieve a row value as a variant, ready to be accessed via late-binding
  7629. // - Row parameter numbering starts from 1 to RowCount
  7630. // - this method will return a TDocVariant containing a copy of all
  7631. // field values of this row, uncoupled to the TSQLTable instance life time
  7632. // - expand* methods would allow to return human-friendly representations
  7633. procedure ToDocVariant(Row: integer; out doc: variant;
  7634. options: TDocVariantOptions=JSON_OPTIONS_FAST;
  7635. expandTimeLogAsText: boolean=false; expandEnumsAsText: boolean=false;
  7636. expandHugeIDAsUniqueIdentifier: boolean=false); overload;
  7637. /// retrieve all row values as a dynamic array of variants, ready to be
  7638. // accessed via late-binding
  7639. // - if readonly is TRUE, will contain an array of TSQLTableRowVariant, which
  7640. // will point directly to the TSQLTable, which should remain allocated
  7641. // - if readonly is FALSE, will contain an array of TDocVariant, containing
  7642. // a copy of all field values of this row, uncoupled to the TSQLTable instance
  7643. // - readonly=TRUE is faster to allocate (around 4 times for 10,000 rows), but
  7644. // may be slightly slower to access than readonly=FALSE, if all values are
  7645. // likely be accessed later in the process
  7646. procedure ToDocVariant(out docs: TVariantDynArray; readonly: boolean); overload;
  7647. /// retrieve all row values as a TDocVariant of kind dvArray, ready to be
  7648. // accessed via late-binding
  7649. // - if readonly is TRUE, will contain an array of TSQLTableRowVariant, which
  7650. // will point directly to the TSQLTable, which should remain allocated
  7651. // - if readonly is FALSE, will contain an array of TDocVariant, containing
  7652. // a copy of all field values of this row, uncoupled to the TSQLTable instance
  7653. // - readonly=TRUE is faster to allocate (around 4 times for 10,000 rows), but
  7654. // may be slightly slower to access than readonly=FALSE, if all values are
  7655. // likely be accessed later in the process
  7656. procedure ToDocVariant(out docarray: variant; readonly: boolean); overload;
  7657. // {$ifdef HASINLINE}inline;{$endif} won't reset docarray as required
  7658. {$endif NOVARIANTS}
  7659. /// save the table values in JSON format
  7660. // - JSON data is added to TStream, with UTF-8 encoding
  7661. // - if Expand is true, JSON data is an array of objects, for direct use
  7662. // with any Ajax or .NET client:
  7663. // & [ {"col1":val11,"col2":"val12"},{"col1":val21,... ]
  7664. // - if Expand is false, JSON data is serialized (used in TSQLTableJSON)
  7665. // & { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] }
  7666. // - RowFirst and RowLast can be used to ask for a specified row extent
  7667. // of the returned data (by default, all rows are retrieved)
  7668. procedure GetJSONValues(JSON: TStream; Expand: boolean;
  7669. RowFirst: integer=0; RowLast: integer=0); overload;
  7670. /// same as the overloaded method, but returning result into a RawUTF8
  7671. function GetJSONValues(Expand: boolean): RawUTF8; overload;
  7672. /// same as the overloaded method, but appending an array to a TTextWriter
  7673. // - will call W.FlushToStream, then append all content
  7674. procedure GetJSONValues(W: TTextWriter; Expand: boolean;
  7675. RowFirst: integer=0; RowLast: integer=0); overload;
  7676. /// save the table as CSV format, into a stream
  7677. // - if Tab=TRUE, will use TAB instead of ',' between columns
  7678. // - you can customize the ',' separator - use e.g. the global ListSeparator
  7679. // variable (from SysUtils) to reflect the current system definition (some
  7680. // country use ',' as decimal separator, for instance our "douce France")
  7681. // - AddBOM will add a UTF-8 Byte Order Mark at the beginning of the content
  7682. procedure GetCSVValues(Dest: TStream; Tab: boolean; CommaSep: AnsiChar=',';
  7683. AddBOM: boolean=false; RowFirst: integer=0; RowLast: integer=0); overload;
  7684. /// save the table as CSV format, into a string variable
  7685. // - if Tab=TRUE, will use TAB instead of ',' between columns
  7686. // - you can customize the ',' separator - use e.g. the global ListSeparator
  7687. // variable (from SysUtils) to reflect the current system definition (some
  7688. // country use ',' as decimal separator, for instance our "douce France")
  7689. // - AddBOM will add a UTF-8 Byte Order Mark at the beginning of the content
  7690. function GetCSVValues(Tab: boolean; CommaSep: AnsiChar=',';
  7691. AddBOM: boolean=false; RowFirst: integer=0; RowLast: integer=0): RawUTF8; overload;
  7692. /// save the table in 'schemas-microsoft-com:rowset' XML format
  7693. // - this format is used by ADODB.recordset, easily consummed by MS apps
  7694. // - see @http://synopse.info/forum/viewtopic.php?pid=11691#p11691
  7695. procedure GetMSRowSetValues(Dest: TStream; RowFirst,RowLast: integer); overload;
  7696. /// save the table in 'schemas-microsoft-com:rowset' XML format
  7697. // - this format is used by ADODB.recordset, easily consummed by MS apps
  7698. // - see @http://synopse.info/forum/viewtopic.php?pid=11691#p11691
  7699. function GetMSRowSetValues: RawUTF8; overload;
  7700. /// save the table in Open Document Spreadsheet compressed format
  7701. // - this is a set of XML files compressed in a zip container
  7702. // - this method will return the raw binary buffer of the file
  7703. // - see @http://synopse.info/forum/viewtopic.php?id=2133
  7704. function GetODSDocument: RawByteString;
  7705. /// append the table content as a HTML <table> ... </table>
  7706. procedure GetHtmlTable(Dest: TTextWriter); overload;
  7707. /// save the table as a <html><body><table> </table></body></html> content
  7708. function GetHtmlTable(const Header: RawUTF8='<head><style>table,th,td'+
  7709. '{border: 1px solid black;border-collapse: collapse;}th,td{padding: 5px;'+
  7710. 'font-family: sans-serif;}</style></head>'#10): RawUTF8; overload;
  7711. /// get the Field index of a FieldName
  7712. // - return -1 if not found, index (0..FieldCount-1) if found
  7713. function FieldIndex(FieldName: PUTF8Char): integer; overload;
  7714. /// get the Field index of a FieldName
  7715. // - return -1 if not found, index (0..FieldCount-1) if found
  7716. function FieldIndex(const FieldName: RawUTF8): integer; overload;
  7717. {$ifdef HASINLINE}inline;{$endif}
  7718. /// get the Field index of a FieldName
  7719. // - raise an ESQLTableException if not found, index (0..FieldCount-1) if found
  7720. function FieldIndexExisting(const FieldName: RawUTF8): integer; overload;
  7721. /// get the Field indexes of several Field names
  7722. // - could be used to speed-up field access in a TSQLTable loop, avoiding
  7723. // a FieldIndex(aFieldName) lookup for each value
  7724. // - return -1 in FieldIndexes[]^ if not found, index (0..FieldCount-1) if found
  7725. procedure FieldIndex(const FieldNames: array of RawUTF8;
  7726. const FieldIndexes: array of PInteger); overload;
  7727. /// get the Field indexes of several Field names
  7728. // - raise an ESQLTableException if not found
  7729. // - set FieldIndexes[]^ to the index (0..FieldCount-1) if found
  7730. // - could be used to speed-up field access in a TSQLTable loop, avoiding
  7731. // a FieldIndex(aFieldName) lookup for each value, as such:
  7732. //! list := TSQLTableJSON.Create('',pointer(json),length(json));
  7733. //! list.FieldIndexExisting(
  7734. //! ['FirstName','LastName','YearOfBirth','YearOfDeath','RowID','Data'],
  7735. //! [@FirstName,@LastName,@YearOfBirth,@YearOfDeath,@RowID,@Data]);
  7736. //! for i := 1 to list.RowCount do begin
  7737. //! Check(list.Get(i,FirstName)<>nil);
  7738. //! Check(list.Get(i,LastName)<>nil);
  7739. //! Check(list.GetAsInteger(i,YearOfBirth)<10000);
  7740. procedure FieldIndexExisting(const FieldNames: array of RawUTF8;
  7741. const FieldIndexes: array of PInteger); overload;
  7742. /// retrieve all field names as a RawUTF8 dynamic array
  7743. function FieldNames: TRawUTF8DynArray;
  7744. /// get the Field content (encoded as UTF-8 text) from a property name
  7745. // - return nil if not found
  7746. function FieldValue(const FieldName: RawUTF8; Row: integer): PUTF8Char;
  7747. {$ifdef HASINLINE}inline;{$endif}
  7748. /// sort result Rows, according to a specific field
  7749. // - default is sorting by ascending order (Asc=true)
  7750. // - you can specify a Row index to be updated during the sort in PCurrentRow
  7751. // - sort is very fast, even for huge tables (more faster than any indexed
  7752. // SQL query): 500,000 rows are sorted instantly
  7753. // - this optimized sort implementation does the comparaison first by the
  7754. // designed field, and, if the field value is identical, the ID value is
  7755. // used (it will therefore sort by time all identical values)
  7756. procedure SortFields(Field: integer; Asc: boolean=true;
  7757. PCurrentRow: PInteger=nil; FieldType: TSQLFieldType=sftUnknown;
  7758. CustomCompare: TUTF8Compare=nil); overload;
  7759. /// sort result Rows, according to a specific field
  7760. // - overloaded method allowing to specify the field by its name
  7761. procedure SortFields(const FieldName: RawUTF8; Asc: boolean=true;
  7762. PCurrentRow: PInteger=nil; FieldType: TSQLFieldType=sftUnknown;
  7763. CustomCompare: TUTF8Compare=nil); overload;
  7764. /// sort result Rows, according to some specific fields
  7765. // - is able to make multi-field sort
  7766. // - both Fields[] and Asc[] array should have the same count, otherwise
  7767. // default Asc[]=true value will be assumed
  7768. // - set any Fields[]=-1 to identify the ID column (even if is hidden)
  7769. procedure SortFields(const Fields: array of integer;
  7770. const Asc: array of boolean); overload;
  7771. /// sort result Rows, according to the Bits set to 1 first
  7772. procedure SortBitsFirst(var Bits);
  7773. /// guess the field type from first non null data row
  7774. // - if QueryTables[] are set, exact field type and enumerate TypeInfo() is
  7775. // retrieved from the Delphi RTTI; otherwise, get from the cells content
  7776. // - return sftUnknown is all data fields are null
  7777. // - sftBlob is returned if the field is encoded as SQLite3 BLOB literals
  7778. // (X'53514C697465' e.g.)
  7779. // - since TSQLTable data is PUTF8Char, string type is sftUTF8Text only
  7780. function FieldType(Field: integer): TSQLFieldType; overload;
  7781. /// guess the field type from first non null data row
  7782. // - if QueryTables[] are set, exact field type and (enumerate) TypeInfo() is
  7783. // retrieved from the Delphi RTTI; otherwise, get from the cells content
  7784. // - return sftUnknown is all data fields are null
  7785. // - sftBlob is returned if the field is encoded as SQLite3 BLOB literals
  7786. // (X'53514C697465' e.g.)
  7787. // - since TSQLTable data is PUTF8Char, string type is sftUTF8Text only
  7788. function FieldType(Field: integer; OutFieldTypeInfo: PPointer): TSQLFieldType; overload;
  7789. /// get the appropriate Sort comparaison function for a field,
  7790. // nil if not available (bad field index or field is blob)
  7791. // - field type is guessed from first data row
  7792. function SortCompare(Field: integer): TUTF8Compare;
  7793. /// get the mean of characters length of all fields
  7794. // - the character length is for the first line of text only (stop counting
  7795. // at every newline character, i.e. #10 or #13 char)
  7796. // - return the sum of all mean of character lengths
  7797. function CalculateFieldLengthMean(var aResult: TIntegerDynArray;
  7798. FromDisplay: boolean=false): integer;
  7799. /// get the mean of characters length of this field
  7800. // - the character length is for the first line of text only (stop counting
  7801. // at every newline character, i.e. #10 or #13 char)
  7802. // - very fast: calculated only once for all fields
  7803. function FieldLengthMean(Field: integer): cardinal;
  7804. /// get the sum of all mean of characters length of all fields
  7805. // - very fast: calculated only once for all fields
  7806. function FieldLengthMeanSum: cardinal;
  7807. /// get the maximum number of characters of this field
  7808. function FieldLengthMax(Field: integer; NeverReturnsZero: boolean=false): cardinal;
  7809. /// get the record class (i.e. the table) associated to a field
  7810. // - is nil if this table has no QueryTables property
  7811. // - very fast: calculated only once for all fields
  7812. function FieldTable(Field: integer): TSQLRecordClass;
  7813. /// force the mean of characters length for every field
  7814. // - expect as many parameters as fields in this table
  7815. // - override internal fFieldLengthMean[] and fFieldLengthMeanSum values
  7816. procedure SetFieldLengthMean(const Lengths: array of cardinal);
  7817. /// set the exact type of a given field
  7818. // - by default, column types and sizes will be retrieved from JSON content
  7819. // from first row, or all rows if FieldTypeIntegerDetectionOnAllRows is set
  7820. // - you can define a specific type for a given column, and optionally
  7821. // a maximum column size
  7822. // - FieldTypeInfo can be specified for sets or enumerations, as such:
  7823. // ! aTable.SetFieldType(0,sftEnumerate,TypeInfo(TEnumSample));
  7824. // ! aTable.SetFieldType(1,sftSet,TypeInfo(TSetSamples));
  7825. // or for dynamic arrays
  7826. procedure SetFieldType(Field: integer; FieldType: TSQLFieldType;
  7827. FieldTypeInfo: pointer=nil; FieldSize: integer=-1;
  7828. FieldTableIndex: integer=-1); overload;
  7829. /// set the exact type of a given field
  7830. // - by default, column types and sizes will be retrieved from JSON content
  7831. // from first row, or all rows if FieldTypeIntegerDetectionOnAllRows is set
  7832. // - you can define a specific type for a given column, and optionally
  7833. // a maximum column size
  7834. // - FieldTypeInfo can be specified for sets or enumerations, as such:
  7835. // ! aTable.SetFieldType('Sample',sftEnumerate,TypeInfo(TEnumSample));
  7836. // ! aTable.SetFieldType('Samples',sftSet,TypeInfo(TSetSamples));
  7837. procedure SetFieldType(const FieldName: RawUTF8; FieldType: TSQLFieldType;
  7838. FieldTypeInfo: pointer=nil; FieldSize: integer=-1); overload;
  7839. /// increase a particular Field Length Mean value
  7840. // - to be used to customize the field appareance (e.g. for adding of left
  7841. // checkbox for Marked[] fields)
  7842. procedure FieldLengthMeanIncrease(aField, aIncrease: integer);
  7843. /// copy the parameters of a TSQLTable into this instance
  7844. // - the fResults remain in the source TSQLTable: source TSQLTable has not to
  7845. // be destroyed before this TSQLTable
  7846. procedure Assign(source: TSQLTable);
  7847. /// search a text value inside the table data in a specified field
  7848. // - the text value must already be uppercased 7-bits ANSI encoded
  7849. // - return the Row on success, 0 on error
  7850. // - search only in the content of FieldIndex data
  7851. // - you can specify a Soundex pronunciation to use, or leave as sndxNone for
  7852. // standard case insensitive character match; aUpperValue can optional
  7853. // indicate a Soundex search, by predeceding the searched text with % for
  7854. // English, %% for French or %%% for Spanish (only works with WinAnsi
  7855. // char set - i.e. code page 1252)
  7856. // - if UnicodeComparison is set to TRUE, search will use low-level Windows
  7857. // API for Unicode-level conversion - it will be much slower, but accurate
  7858. // for the whole range of UTF-8 encoding
  7859. // - if UnicodeComparison is left to FALSE, UTF-8 decoding will be done only
  7860. // if necessary: it will work only with standard western-occidental alphabet
  7861. // (i.e. WinAnsi - code page 1252), but it will be very fast
  7862. function SearchValue(const aUpperValue: RawUTF8; StartRow, FieldIndex: integer;
  7863. Client: TObject; Lang: TSynSoundExPronunciation=sndxNone;
  7864. UnicodeComparison: boolean=false): integer; overload;
  7865. /// search a text value inside the table data in all fields
  7866. // - the text value must already be uppercased 7-bits ANSI encoded
  7867. // - return the Row on success, 0 on error
  7868. // - search on all fields, returning field found in FieldIndex (if not nil)
  7869. // - you can specify a Soundex pronunciation to use, or leave as sndxNone for
  7870. // standard case insensitive character match; aUpperValue can optional
  7871. // indicate a Soundex search, by predeceding the searched text with % for
  7872. // English, %% for French or %%% for Spanish (only works with WinAnsi
  7873. // char set - i.e. code page 1252)
  7874. // - if UnicodeComparison is set to TRUE, search will use low-level Windows
  7875. // API for Unicode-level conversion - it will be much slower, but accurate
  7876. // for the whole range of UTF-8 encoding
  7877. // - if UnicodeComparison is left to FALSE, UTF-8 decoding will be done only
  7878. // if necessary: it will work only with standard western-occidental alphabet
  7879. // (i.e. WinAnsi - code page 1252), but it will be very fast
  7880. function SearchValue(const aUpperValue: RawUTF8; StartRow: integer;
  7881. FieldIndex: PInteger; Client: TObject; Lang: TSynSoundExPronunciation=sndxNone;
  7882. UnicodeComparison: boolean=false): integer; overload;
  7883. /// search for a value inside the raw table data
  7884. // - returns 0 if not found, or the matching Row number otherwise
  7885. function SearchFieldEquals(const aValue: RawUTF8; FieldIndex: integer): integer;
  7886. /// if the ID column is available, hides it from fResults[]
  7887. // - useful for simplier UI, with a hidden ID field
  7888. // - use IDColumnHiddenValue() to get the ID of a specific row
  7889. // - return true is ID was succesfully hidden, false if not possible
  7890. function IDColumnHide: boolean;
  7891. /// return the (previously hidden) ID value, 0 on error
  7892. function IDColumnHiddenValue(Row: integer): TID;
  7893. /// return all (previously hidden) ID values
  7894. procedure IDColumnHiddenValues(var IDs: TIDDynArray);
  7895. /// get all IDs where individual bit in Bits are set
  7896. procedure IDArrayFromBits(const Bits; var IDs: TIDDynArray);
  7897. /// get all individual bit in Bits corresponding to the supplied IDs
  7898. // - warning: IDs integer array will be sorted within this method call
  7899. procedure IDArrayToBits(var Bits; var IDs: TIDDynArray);
  7900. /// get the Row index corresponding to a specified ID
  7901. // - return the Row number, from 1 to RowCount
  7902. // - return RowCount (last row index) if this ID was not found or no
  7903. // ID field is available
  7904. function RowFromID(aID: TID): integer;
  7905. /// delete the specified data Row from the Table
  7906. // - only overwrite the internal fResults[] pointers, don't free any memory,
  7907. // nor modify the internal DataSet
  7908. procedure DeleteRow(Row: integer);
  7909. /// delete the specified Column text from the Table
  7910. // - don't delete the Column: only delete UTF-8 text in all rows for this field
  7911. procedure DeleteColumnValues(Field: integer);
  7912. /// retrieve QueryTables[0], if existing
  7913. function QueryRecordType: TSQLRecordClass;
  7914. /// create a new TSQLRecord instance for a specific Table
  7915. // - use the specified TSQLRecord class or create one instance
  7916. // of the first associated record class (from internal QueryTables[])
  7917. // - use this method to create a working copy of a table's record, e.g.
  7918. // - the record will be freed when the TSQLTable will be destroyed:
  7919. // you don't need to make a Try..Finally..Free..end block with it
  7920. function NewRecord(RecordType: TSQLRecordClass=nil): TSQLRecord;
  7921. /// create a TObjectList with TSQLRecord instances corresponding to this
  7922. // TSQLTable result set
  7923. // - use the specified TSQLRecord class or create instances
  7924. // of the first associated record class (from internal QueryTables[])
  7925. // - always returns an instance, even if the TSQLTable is nil or void
  7926. function ToObjectList(RecordType: TSQLRecordClass=nil): TObjectList; overload;
  7927. /// fill an existing TObjectList with TSQLRecord instances corresponding
  7928. // to this TSQLTable result set
  7929. // - use the specified TSQLRecord class or create instances
  7930. // of the first associated record class (from internal QueryTables[])
  7931. procedure ToObjectList(DestList: TObjectList; RecordType: TSQLRecordClass=nil); overload;
  7932. {$ifdef ISDELPHI2010} // Delphi 2009 generics are buggy
  7933. /// create a TObjectList<TSQLRecord> with TSQLRecord instances corresponding
  7934. // to this TSQLTable result set
  7935. // - use the specified TSQLRecord class or create instances
  7936. // of the first associated record class (from internal QueryTables[])
  7937. // - always returns an instance, even if the TSQLTable is nil or void
  7938. function ToObjectList<T: TSQLRecord>: TObjectList<T>; overload;
  7939. {$endif}
  7940. /// fill an existing T*ObjArray variable with TSQLRecord instances
  7941. // corresponding to this TSQLTable result set
  7942. // - use the specified TSQLRecord class or create instances
  7943. // of the first associated record class (from internal QueryTables[])
  7944. // - returns TRUE on success (even if ObjArray=[]), FALSE on error
  7945. function ToObjArray(var ObjArray; RecordType: TSQLRecordClass=nil): boolean;
  7946. /// After a TSQLTable has been initialized, this method can be called
  7947. // one or more times to iterate through all data rows
  7948. // - you shall call this method before calling FieldBuffer()/Field() methods
  7949. // - return TRUE on success, with data ready to be retrieved by Field*()
  7950. // - return FALSE if no more row is available (i.e. exceeded RowCount)
  7951. // - if SeekFirst is TRUE, will put the cursor on the first row of results,
  7952. // otherwise, it will fetch one row of data, to be called within a loop
  7953. // - you can specify a variant instance (e.g. allocated on the stack) in
  7954. // optional RowVariant parameter, to access field values using late binding
  7955. // - typical use may be:
  7956. // ! while TableCustomers.Step do
  7957. // ! writeln(Field('name'));
  7958. // - or, when using a variant and late-binding:
  7959. // ! var customer: variant;
  7960. // ! ...
  7961. // ! while TableCustomers.Step(false,@customer) do
  7962. // ! writeln(customer.Name);
  7963. function Step(SeekFirst: boolean=false; RowVariant: PVariant=nil): boolean;
  7964. /// read-only access to a particular field value, as UTF-8 encoded buffer
  7965. // - raise an ESQLTableException if called outside valid Step() sequence
  7966. // - similar to Get() method, but for the current Step
  7967. function FieldBuffer(FieldIndex: Integer): PUTF8Char; overload;
  7968. /// read-only access to a particular field value, as UTF-8 encoded buffer
  7969. // - raise an ESQLTableException if called outside valid Step() sequence
  7970. // - similar to Get() method, but for the current Step
  7971. function FieldBuffer(const FieldName: RawUTF8): PUTF8Char; overload;
  7972. /// read-only access to a particular field value, as Integer
  7973. // - raise an ESQLTableException if called outside valid Step() sequence
  7974. // - similar to GetAsInteger() method, but for the current Step
  7975. function FieldAsInteger(FieldIndex: Integer): Int64; overload;
  7976. {$ifdef HASINLINE}inline;{$endif}
  7977. /// read-only access to a particular field value, as Integer
  7978. // - raise an ESQLTableException if called outside valid Step() sequence
  7979. // - similar to GetAsInteger() method, but for the current Step
  7980. function FieldAsInteger(const FieldName: RawUTF8): Int64; overload;
  7981. {$ifdef HASINLINE}inline;{$endif}
  7982. /// read-only access to a particular field value, as floating-point value
  7983. // - raise an ESQLTableException if called outside valid Step() sequence
  7984. // - similar to GetAsFloat() method, but for the current Step
  7985. function FieldAsFloat(FieldIndex: Integer): TSynExtended; overload;
  7986. {$ifdef HASINLINE}inline;{$endif}
  7987. /// read-only access to a particular field value, as floating-point value
  7988. // - raise an ESQLTableException if called outside valid Step() sequence
  7989. // - similar to GetAsFloat() method, but for the current Step
  7990. function FieldAsFloat(const FieldName: RawUTF8): TSynExtended; overload;
  7991. {$ifdef HASINLINE}inline;{$endif}
  7992. {$ifndef NOVARIANTS}
  7993. /// read-only access to a particular field value, as a variant
  7994. // - raise an ESQLTableException if called outside valid Step() sequence
  7995. // - will call GetVariant() method for appropriate data conversion
  7996. function Field(FieldIndex: integer): variant; overload;
  7997. /// read-only access to a particular field value, as a variant
  7998. // - raise an ESQLTableException if called outside valid Step() sequence
  7999. // - will call GetVariant() method for appropriate data conversion
  8000. function Field(const FieldName: RawUTF8): variant; overload;
  8001. {$endif}
  8002. /// contains the associated record class on Query
  8003. property QueryTables: TSQLRecordClassDynArray read fQueryTables;
  8004. /// contains the associated SQL statement on Query
  8005. property QuerySQL: RawUTF8 read fQuerySQL;
  8006. /// returns the SQL Table name, guessed from the associated QuerySQL statement
  8007. property QueryTableNameFromSQL: RawUTF8 read GetQueryTableNameFromSQL;
  8008. /// read-only access to the number of data Rows in this table
  8009. // - first row contains field name
  8010. // - then 1..RowCount rows contain the data itself
  8011. property RowCount: integer read GetRowCount;
  8012. /// read-only access to the number of fields for each Row in this table
  8013. property FieldCount: integer read fFieldCount;
  8014. /// read-only access to the ID/RowID field index
  8015. // - do not use this property if the ID column has been hidden, but
  8016. // use IDColumnHiddenValue() method instead
  8017. property FieldIndexID: integer read fFieldIndexID;
  8018. /// read-only acccess to the current Row number, after a Step() call
  8019. // - contains 0 if accessed outside valid Step() sequence call
  8020. // - contains 1..RowCount after a valid Step() iteration
  8021. property StepRow: integer read fStepRow;
  8022. /// this property contains the internal state counter of the server database
  8023. // when the data was retrieved from it
  8024. // - can be used to check if retrieved data may be out of date
  8025. property InternalState: cardinal read fInternalState write fInternalState;
  8026. /// if the TSQLRecord is the owner of this table, i.e. if it must free it
  8027. property OwnerMustFree: Boolean read fOwnerMustFree write fOwnerMustFree;
  8028. /// by default, if field types are not set, only the content of the first
  8029. // row will be checked, to make a difference between a sftInteger and sftFloat
  8030. // - you can set this property to TRUE so that all non string rows will
  8031. // be checked for the exact number precision
  8032. // - note that the safest is to provide the column type, either by supplying
  8033. // the TSQLRecord class, or by calling SetFieldType() overloaded methods
  8034. property FieldTypeIntegerDetectionOnAllRows: boolean
  8035. read fFieldTypeAllRows write fFieldTypeAllRows;
  8036. /// used by GetJsonValues, GetHtmlTable and GetCSVValues methods
  8037. // to export custom JSON content
  8038. property OnExportValue: TOnSQLTableGetValue read fOnExportValue write fOnExportValue;
  8039. end;
  8040. {$ifndef NOVARIANTS}
  8041. /// memory structure used for our TSQLTableRowVariant custom variant type
  8042. // used to have direct access to TSQLTable content
  8043. // - the associated TSQLTable must stay allocated as long as this variant
  8044. // is used, otherwise random GPF issues may occur
  8045. TSQLTableRowVariantData = packed record
  8046. /// the custom variant type registered number
  8047. VType: TVarType;
  8048. VFiller: array[1..sizeof(TVarData)-sizeof(TVarType)-sizeof(TSQLTable)
  8049. -sizeof(integer)] of byte;
  8050. /// reference to the associated TSQLTable
  8051. VTable: TSQLTable;
  8052. /// the row number corresponding to this value
  8053. // - equals -1 if should follow StepRow property value
  8054. VRow: integer;
  8055. end;
  8056. /// pointer to the memory structure used for TSQLTableRowVariant storage
  8057. PSQLTableRowVariantData = ^TSQLTableRowVariantData;
  8058. /// a custom variant type used to have direct access to TSQLTable content
  8059. // - use TSQLTable.Step(..,@Data) method to initialize such a Variant
  8060. // - the variant members/fields are read-only by design
  8061. // - the associated TSQLTable must stay allocated as long as this variant
  8062. // is used, otherwise random GPF issues may occur
  8063. TSQLTableRowVariant = class(TSynInvokeableVariantType)
  8064. protected
  8065. procedure IntGet(var Dest: TVarData; const V: TVarData; Name: PAnsiChar); override;
  8066. procedure IntSet(const V, Value: TVarData; Name: PAnsiChar); override;
  8067. public
  8068. /// customization of variant into JSON serialization
  8069. procedure ToJSON(W: TTextWriter; const Value: variant; Escape: TTextWriterKind); override;
  8070. /// handle type conversion to string
  8071. procedure Cast(var Dest: TVarData; const Source: TVarData); override;
  8072. /// handle type conversion to string
  8073. procedure CastTo(var Dest: TVarData; const Source: TVarData;
  8074. const AVarType: TVarType); override;
  8075. end;
  8076. {$endif NOVARIANTS}
  8077. /// get a SQL result from a JSON message, and store it into its own memory
  8078. TSQLTableJSON = class(TSQLTable)
  8079. protected
  8080. /// used if a private copy of the JSON buffer is needed
  8081. fPrivateCopy: RawUTF8;
  8082. /// contains the pointers of start of every field value in JSONData
  8083. fJSONResults: array of PUTF8Char;
  8084. /// contain the hash value of the last JSON data sent to ContentChanged()
  8085. // - used to don't repeat parsing if data has not been changed
  8086. fPrivateCopyHash: cardinal;
  8087. /// fill the result table content from a JSON-formated Data message
  8088. // - returns TRUE on parsing success
  8089. // - returns FALSE if no valid JSON data was found
  8090. // - update all content fields (fResults[], fRowCount, fFieldCount, etc...)
  8091. // - expect the UTF-8 Buffer in either TSQLRequest.EngineExecute(DB,SQL,JSON)
  8092. // format (i.e. expanded) or either in a not expanded format (as an
  8093. // AJAX-ready array of objects)
  8094. // - the conversion into PPUTF8CharArray is made inplace and is very fast
  8095. // (no additional memory buffer is allocated)
  8096. function ParseAndConvert(Buffer: PUTF8Char; BufferLen: integer): boolean;
  8097. /// will check then set (if needed) internal fPrivateCopy[Hash] values
  8098. // - returns TRUE if content changed (then fPrivateCopy+fPrivateCopyHash
  8099. // will be updated using crc32c hash)
  8100. function PrivateCopyChanged(aJSON: PUTF8Char; aLen: integer): boolean;
  8101. public
  8102. /// create the result table from a JSON-formated Data message
  8103. // - the JSON data is parsed and formatted in-place
  8104. // - please note that the supplied JSON buffer content will be changed:
  8105. // if you want to reuse this JSON content, you shall make a private copy
  8106. // before calling this constructor and you shall NOT release the corresponding
  8107. // variable (fResults/JSONResults[] will point inside this memory buffer):
  8108. // use instead the overloaded Create constructor expecting aJSON parameter
  8109. // making a private copy of the data
  8110. constructor Create(const aSQL: RawUTF8;
  8111. JSONBuffer: PUTF8Char; JSONBufferLen: integer); reintroduce; overload;
  8112. /// create the result table from a JSON-formated Data message
  8113. // - the JSON data is parsed and formatted in-place, after having been
  8114. // copied in the protected fPrivateCopy variable
  8115. constructor Create(const aSQL, aJSON: RawUTF8); reintroduce; overload;
  8116. /// create the result table from a JSON-formated Data message
  8117. // - the JSON data is parsed and formatted in-place
  8118. // - you can specify a set of TSQLRecord classes which will be used to
  8119. // retrieve the column exact type information
  8120. // - please note that the supplied JSON buffer content will be changed
  8121. constructor CreateFromTables(const Tables: array of TSQLRecordClass;
  8122. const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); reintroduce; overload;
  8123. /// create the result table from a JSON-formated Data message
  8124. // - you can specify a set of TSQLRecord classes which will be used to
  8125. // retrieve the column exact type information
  8126. // - the JSON data is parsed and formatted in-place, after having been
  8127. // copied in the protected fPrivateCopy variable
  8128. constructor CreateFromTables(const Tables: array of TSQLRecordClass;
  8129. const aSQL, aJSON: RawUTF8); reintroduce; overload;
  8130. /// initialize the result table from a JSON-formated Data message
  8131. // - you can set the expected column types matching the results column layout
  8132. // - the JSON data is parsed and formatted in-place
  8133. constructor CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType;
  8134. const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); reintroduce; overload;
  8135. /// initialize the result table from a JSON-formated Data message
  8136. // - you can set the expected column types matching the results column layout
  8137. // - the JSON data is parsed and formatted in-place, after having been
  8138. // copied in the protected fPrivateCopy variable
  8139. constructor CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType;
  8140. const aSQL, aJSON: RawUTF8); reintroduce; overload;
  8141. /// update the result table content from a JSON-formated Data message
  8142. // - return true on parsing success, false if no valid JSON data was found
  8143. // - set Refreshed to true if the content changed
  8144. // - update all content fields (fResults[], fRowCount, fFieldCount, etc...)
  8145. // - call SortFields() or IDColumnHide if was already done for this TSQLTable
  8146. // - the conversion into PPUTF8CharArray is made inplace and is very fast
  8147. // (only one memory buffer is allocated for the whole data)
  8148. function UpdateFrom(const aJSON: RawUTF8; var Refreshed: boolean;
  8149. PCurrentRow: PInteger): boolean;
  8150. /// the private copy of the processed data buffer
  8151. // - available e.g. for Create constructor using aJSON parameter,
  8152. // or after the UpdateFrom() process
  8153. // - this buffer is not to be access directly: this won't be a valid JSON
  8154. // content, but a processed buffer, on which fResults[] elements point to -
  8155. // it will contain unescaped text and numerical values, ending with #0
  8156. property PrivateInternalCopy: RawUTF8 read fPrivateCopy;
  8157. end;
  8158. PSQLLocks = ^TSQLLocks;
  8159. /// used to store the locked record list, in a specified table
  8160. // - the maximum count of the locked list if fixed to 512 by default,
  8161. // which seems correct for common usage
  8162. TSQLLocks = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  8163. /// the number of locked records stored in this object
  8164. Count: integer;
  8165. /// contains the locked record ID
  8166. // - an empty position is marked with 0 after UnLock()
  8167. IDs: TIDDynArray;
  8168. /// contains the time and date of the lock
  8169. // - filled internally by the fast GetTickCount64() function (faster than
  8170. // TDateTime or TSystemTime/GetLocalTime)
  8171. // - used to purge to old entries - see PurgeOlderThan() method below
  8172. Ticks64s: TInt64DynArray;
  8173. /// lock a record, specified by its ID
  8174. // - returns true on success, false if was already locked
  8175. function Lock(aID: TID): boolean;
  8176. /// unlock a record, specified by its ID
  8177. // - returns true on success, false if was not already locked
  8178. function UnLock(aID: TID): boolean;
  8179. /// return true if a record, specified by its ID, is locked
  8180. function isLocked(aID: TID): boolean;
  8181. /// delete all the locked IDs entries, after a specified time
  8182. // - to be used to release locked records if the client crashed
  8183. // - default value is 30 minutes, which seems correct for common database usage
  8184. procedure PurgeOlderThan(MinutesFromNow: cardinal=30);
  8185. end;
  8186. TSQLLocksDynArray = array of TSQLLocks;
  8187. /// UI Query comparison operators
  8188. // - these operators are e.g. used to mark or unmark some lines in a UI Grid
  8189. // or for TInterfaceStub.ExpectsCount() methods
  8190. TSQLQueryOperator =
  8191. (qoNone,
  8192. qoEqualTo,
  8193. qoNotEqualTo,
  8194. qoLessThan,
  8195. qoLessThanOrEqualTo,
  8196. qoGreaterThan,
  8197. qoGreaterThanOrEqualTo,
  8198. qoEqualToWithCase,
  8199. qoNotEqualToWithCase,
  8200. qoContains,
  8201. qoBeginWith,
  8202. qoSoundsLikeEnglish,
  8203. qoSoundsLikeFrench,
  8204. qoSoundsLikeSpanish);
  8205. /// set of UI Query comparison operators
  8206. TSQLQueryOperators = set of TSQLQueryOperator;
  8207. /// User Interface Query action evaluation function prototype
  8208. // - Operator is ord(TSQLQueryOperator) by default (i.e. for class function
  8209. // TSQLRest.QueryIsTrue), or is a custom enumeration index for custom queries
  8210. // (see TSQLQueryCustom.EnumIndex below, and TSQLRest.QueryAddCustom() method)
  8211. // - for default Operator as ord(TSQLQueryOperator), qoContains and qoBeginWith
  8212. // expect the Reference to be already uppercase
  8213. // - qoEqualTo to qoGreaterThanOrEqualTo apply to all field kind (work with
  8214. // either numeric either UTF-8 values)
  8215. // - qoEqualToWithCase to qoSoundsLikeSpanish handle the field as UTF-8 text,
  8216. // and make the comparison using the phonetic algorithm corresponding to
  8217. // a language family
  8218. // - for default Operator as ord(TSQLQueryOperator), qoSoundsLike* operators
  8219. // expect the Reference not to be a PUTF8Char, but a typecast of a prepared
  8220. // TSynSoundEx object instance (i.e. pointer(@SoundEx)) by the caller
  8221. // - for custom query (from TSQLQueryCustom below), the event must
  8222. // handle a special first call with Value=nil to select if this custom
  8223. // Operator/Query is available for the specified aTable: in this case,
  8224. // returning true indicates that this custom query is available for this table
  8225. // - for custom query (from TSQLQueryCustom below), the event is called with
  8226. // FieldType := TSQLFieldType(TSQLQueryCustom.EnumIndex)+64
  8227. TSQLQueryEvent = function(aTable: TSQLRecordClass; aID: TID;
  8228. FieldType: TSQLFieldType; Value: PUTF8Char; Operator: integer;
  8229. Reference: PUTF8Char): boolean of object;
  8230. /// store one custom query parameters
  8231. // - add custom query by using the TSQLRest.QueryAddCustom() method
  8232. // - use EnumType^.GetCaption(EnumIndex) to retrieve the caption associated
  8233. // to this custom query
  8234. TSQLQueryCustom = record
  8235. /// the associated enumeration type
  8236. EnumType: PEnumType;
  8237. /// the associated enumeration index in EnumType
  8238. // - will be used to fill the Operator parameter for the Event call
  8239. EnumIndex: integer;
  8240. /// the associated evaluation Event handler
  8241. // - the Operator parameter will be filled with the EnumIndex value
  8242. Event: TSQLQueryEvent;
  8243. /// User Interface Query action operators
  8244. Operators: TSQLQueryOperators;
  8245. end;
  8246. /// standard actions for User Interface generation
  8247. // - actNoAction for not defined action
  8248. // - actMark (standard action) to Mark rows, i.e. display sub-menu with
  8249. // actmarkAllEntries..actmarkOlderThanOneYear items
  8250. // - actUnmarkAll (standard action) to UnMark all rows
  8251. // - actmarkAllEntries to Mark all rows
  8252. // - actmarkToday to Mark rows for today
  8253. // - actmarkThisWeek to Mark rows for this Week
  8254. // - actmarkThisMonth to Mark rows for this month
  8255. // - actmarkYesterday to Mark rows for today
  8256. // - actmarkLastWeek to Mark rows for Last Week
  8257. // - actmarkLastMonth to Mark rows for Last month
  8258. // - actmarkOlderThanOneDay to Mark rows After one day
  8259. // - actmarkOlderThanOneWeek to Mark rows older than one week
  8260. // - actmarkOlderThanOneMonth to Mark rows older than one month
  8261. // - actmarkOlderThanSixMonths to Mark rows older than one half year
  8262. // - actmarkOlderThanOneYear to Mark rows older than one year
  8263. // - actmarkInverse to Inverse Mark values (ON->OFF, OFF->ON)
  8264. TSQLAction = (
  8265. actNoAction,
  8266. actMark,
  8267. actUnmarkAll,
  8268. actmarkAllEntries,
  8269. actmarkToday,
  8270. actmarkThisWeek,
  8271. actmarkThisMonth,
  8272. actmarkYesterday,
  8273. actmarkLastWeek,
  8274. actmarkLastMonth,
  8275. actmarkOlderThanOneDay,
  8276. actmarkOlderThanOneWeek,
  8277. actmarkOlderThanOneMonth,
  8278. actmarkOlderThanSixMonths,
  8279. actmarkOlderThanOneYear,
  8280. actmarkInverse);
  8281. /// set of standard actions for User Interface generation
  8282. TSQLActions = set of TSQLAction;
  8283. /// how TSQLModel.URIMatch() would compare an URI
  8284. // - will allow to make a difference about case-sensitivity
  8285. TSQLRestModelMatch = (rmNoMatch, rmMatchExact, rmMatchWithCaseChange);
  8286. /// defines the way the TDrawGrid is displayed by User Interface generation
  8287. TSQLListLayout = (llLeft, llUp, llClient, llLeftUp);
  8288. PSQLRibbonTabParameters = ^TSQLRibbonTabParameters;
  8289. /// defines the settings for a Tab for User Interface generation
  8290. // - used in mORMotToolBar.pas unit and TSQLModel.Create() overloaded method
  8291. TSQLRibbonTabParameters = object
  8292. public
  8293. /// the Table associated to this Tab
  8294. Table: TSQLRecordClass;
  8295. /// the caption of the Tab, to be translated on the screen
  8296. // - by default, Tab name is taken from TSQLRecord.Caption(nil) method
  8297. // - but you can override this value by setting a pointer to a resourcestring
  8298. CustomCaption: PResStringRec;
  8299. /// the hint type of the Tab, to be translated on the screen
  8300. // - by default, hint will replace all %s instance by the Tab name, as taken
  8301. // from TSQLRecord.Caption(nil) method
  8302. // - but you can override this value by setting a pointer to a resourcestring
  8303. CustomHint: PResStringRec;
  8304. /// SQL fields to be displayed on the data lists
  8305. // 'ID,' is always added at the beginning
  8306. Select: RawUTF8;
  8307. /// Tab Group number (index starting at 0)
  8308. Group: integer;
  8309. /// displayed field length mean, one char per field (A=1,Z=26)
  8310. // - put lowercase character in order to center the field data
  8311. FieldWidth: RawUTF8;
  8312. /// if set, the ID column is shown
  8313. ShowID: boolean;
  8314. /// index of field used for displaying order
  8315. OrderFieldIndex: integer;
  8316. /// if set, the list is displayed in reverse order (i.e. decreasing)
  8317. ReverseOrder: boolean;
  8318. /// layout of the List, below the ribbon
  8319. Layout: TSQLListLayout;
  8320. /// width of the List, in percent of the client area
  8321. // - default value (as stated in TSQLRibbonTab.Create) is 30%
  8322. ListWidth: integer;
  8323. /// by default, the detail are displayed as a report (TGDIPages component)
  8324. // - set this property to true to customize the details display
  8325. // - this property is ignored if Layout is llClient (i.e. details hidden)
  8326. NoReport: boolean;
  8327. /// by default, the screens are not refreshed automaticaly
  8328. // - but you can enable the auto-refresh feature by setting this
  8329. // property to TRUE, and creating a WM_TIMER message handler for the form,
  8330. // which will handle both WM_TIMER_REFRESH_SCREEN and WM_TIMER_REFRESH_REPORT
  8331. // timers:
  8332. // !procedure TMainForm.WMRefreshTimer(var Msg: TWMTimer);
  8333. // !begin
  8334. // ! Ribbon.WMRefreshTimer(Msg);
  8335. // !end;
  8336. AutoRefresh: boolean;
  8337. /// the associated hints to be displayed during the edition of this table
  8338. // - every field hint must be separated by a '|' character
  8339. // (e.g. 'The First Name|Its Company Name')
  8340. // - all fields need to be listed in this text resource, even if it won't
  8341. // be displayed on screen (enter a void item like ||)
  8342. // - you can define some value by setting a pointer to a resourcestring
  8343. EditFieldHints: PResStringRec;
  8344. /// write hints above field during the edition of this table
  8345. // - if EditExpandFieldHints is TRUE, the hints are written as text on the
  8346. // dialog, just above the field content; by default, hints are displayed as
  8347. // standard delayed popup when the mouse hover the field editor
  8348. EditExpandFieldHints: boolean;
  8349. /// the associated field name width (in pixels) to be used for creating
  8350. // the edition dialog for this table
  8351. EditFieldNameWidth: integer;
  8352. /// a CSV list of field names to be hidden in both editor and default report
  8353. // - handy to hide fields containing JSON data or the name of another
  8354. // sftRecord/sftID/sftTID (i.e. TRecordReference/TSQLRecord props) fields
  8355. // - list is to be separated by commas (e.g. "RunLogJSON,OrdersJSON" or
  8356. // "ConnectionName")
  8357. EditFieldNameToHideCSV: RawUTF8;
  8358. /// if the default report must contain the edit field hints
  8359. // - i.e. if the resourcestring pointed by EditFieldHints must be used
  8360. // to display some text above every property value on the reports
  8361. EditFieldHintsToReport: boolean;
  8362. end;
  8363. /// parent of all virtual classes
  8364. // - you can define a plain TSQLRecord class as virtual if needed - e.g.
  8365. // inheriting from TSQLRecordMany then calling VirtualTableExternalRegister() -
  8366. // but using this class will seal its state to be virtual
  8367. TSQLRecordVirtual = class(TSQLRecord);
  8368. TSQLVirtualTable = class;
  8369. /// class-reference type (metaclass) of a virtual table implementation
  8370. TSQLVirtualTableClass = class of TSQLVirtualTable;
  8371. /// pre-computed SQL statements for ORM operations for a given
  8372. // TSQLModelRecordProperties instance
  8373. TSQLModelRecordPropertiesSQL = record
  8374. /// the simple field names in a SQL SELECT compatible format: 'COL1,COL2' e.g.
  8375. // - format is
  8376. // ! SQL.TableSimpleFields[withID: boolean; withTableName: boolean]
  8377. // - returns '*' if no field is of TSQLRawBlob/TSQLRecordMany kind
  8378. // - returns 'COL1,COL2' with all COL* set to simple field names if withID is false
  8379. // - returns 'ID,COL1,COL2' with all COL* set to simple field names if withID is true
  8380. // - returns 'Table.ID,Table.COL1,Table.COL2' if withTableName and withID are true
  8381. TableSimpleFields: array[boolean,boolean] of RawUTF8;
  8382. /// the SQL statement for reading all simple fields and RowID
  8383. // - to be checked if we may safely call EngineList()
  8384. SelectAllWithRowID: RawUTF8;
  8385. /// the SQL statement for reading all simple fields with ID
  8386. // - to be checked if we may safely call EngineList()
  8387. SelectAllWithID: RawUTF8;
  8388. /// the JOINed SQL statement for reading all fields with ID, including
  8389. // nested TSQLRecord pre-allocated instances
  8390. // - is '' if there is no nested TSQLRecord
  8391. SelectAllJoined: RawUTF8;
  8392. /// the updated simple fields exposed as 'COL1=?,COL2=?'
  8393. // - excluding ID (but including TCreateTime fields - as used in
  8394. // TSQLVirtualTableExternal.Update method)
  8395. // - to be used e.g. for UPDATE statements
  8396. UpdateSetSimple: RawUTF8;
  8397. /// all updated fields exposed as 'COL1=?,COL2=?'
  8398. // - excluding ID (but including TCreateTime fields - as used in
  8399. // TSQLVirtualTableExternal.Update method)
  8400. // - to be used e.g. for UPDATE statements
  8401. UpdateSetAll: RawUTF8;
  8402. /// all fields, excluding the ID field, exposed as 'COL1,COL2'
  8403. // - to be used e.g. in TSQLVirtualTableExternal.Insert()
  8404. InsertSet: RawUTF8;
  8405. end;
  8406. /// used by TSQLRecordPropertiesMapping.Options for custom field mapping
  8407. // of a TSQLRecord on an external database process
  8408. // - rpmAutoMapKeywordFields is set if MapAutoKeywordFields has been defined,
  8409. // i.e. if field names which may conflict with a keyword should be
  8410. // automatically mapped to a harmless symbol name
  8411. // - rpmNoCreateMissingTable would bypass the existing table check, e.g.
  8412. // to circumvent some specific DB provider or case sensitivity issue on tables
  8413. // - rpmNoCreateMissingField would bypass the existing field check, e.g.
  8414. // to circumvent some specific DB provider or case sensitivity issue on fields
  8415. // - by default, check of missing field name would be case insensitive, unless
  8416. // the rpmMissingFieldNameCaseSensitive option is set
  8417. TSQLRecordPropertiesMappingOptions = set of (
  8418. rpmAutoMapKeywordFields,
  8419. rpmNoCreateMissingTable, rpmNoCreateMissingField,
  8420. rpmMissingFieldNameCaseSensitive);
  8421. /// pointer to external database properties for ORM
  8422. // - is used e.g. to allow a "fluent" interface for MapField() method
  8423. PSQLRecordPropertiesMapping = ^TSQLRecordPropertiesMapping;
  8424. /// allow custom field mapping of a TSQLRecord
  8425. // - used e.g. for external database process, including SQL generation,
  8426. // as implemented in the mORMotDB.pas unit
  8427. // - in end user code, mostly MapField/MapFields/Options methods
  8428. // should be used, if needed as a fluent chained interface - other lower
  8429. // level methods will be used by the framework internals
  8430. {$ifndef ISDELPHI2010}
  8431. TSQLRecordPropertiesMapping = object
  8432. {$else}
  8433. TSQLRecordPropertiesMapping = record
  8434. {$endif}
  8435. private
  8436. /// storage of main read-only properties
  8437. fProps: TSQLRecordProperties;
  8438. fConnectionProperties: TObject;
  8439. fTableName: RawUTF8;
  8440. fRowIDFieldName: RawUTF8;
  8441. fFieldNames: TRawUTF8DynArray;
  8442. fSQL: TSQLModelRecordPropertiesSQL;
  8443. fFieldNamesMatchInternal: TSQLFieldBits;
  8444. fOptions: TSQLRecordPropertiesMappingOptions;
  8445. fAutoComputeSQL: boolean;
  8446. fMappingVersion: cardinal;
  8447. /// fill fRowIDFieldName/fSQL with the current information
  8448. procedure ComputeSQL;
  8449. public
  8450. /// add a custom field mapping
  8451. // - will re-compute all needed SQL statements as needed, and initialize
  8452. // fSortedFieldsName[] and fSortedFieldsIndex[] internal sorted arrays
  8453. // - can be used e.g. as
  8454. // ! aModel.Props[TSQLMyExternal].ExternalDB.MapField('IntField','ExtField');
  8455. // - since it returns a PSQLRecordPropertiesMapping instance, you can
  8456. // chain MapField().MapField().MapField(); calls to map several fields
  8457. function MapField(const InternalName, ExternalName: RawUTF8): PSQLRecordPropertiesMapping;
  8458. /// call this method to ensure that all fields won't conflict with a SQL
  8459. // keyword for the given database
  8460. // - by default, no check is performed: you can use this method to ensure
  8461. // that all field names won't conflict with a SQL reserved keyword: such
  8462. // fields will be identified and automatically mapped as fieldname_
  8463. // - can be used e.g. as
  8464. // ! aModel.Props[TSQLMyExternal].ExternalDB.
  8465. // ! MapField('IntField','ExtField').
  8466. // ! MapAutoKeywordFields;
  8467. // - will in fact include the rpmAutoMapKeywordFields flag in Options
  8468. // - since it returns a PSQLRecordPropertiesMapping instance, you can
  8469. // chain MapField().MapAutoKeywordFields.MapField(); calls to map several fields
  8470. function MapAutoKeywordFields: PSQLRecordPropertiesMapping;
  8471. /// specify some advanced options for the field mapping
  8472. // - see TSQLRecordPropertiesMappingOptions for all possibilities
  8473. // - can be used e.g. as
  8474. // ! aModel.Props[TSQLMyExternal].ExternalDB.
  8475. // ! MapField('IntField','ExtField').
  8476. // ! SetOptions([rpmNoCreateMissingTable,rpmNoCreateMissingField]);
  8477. // - since it returns a PSQLRecordPropertiesMapping instance, you can
  8478. // chain MapField().SetOptions().MapField(); calls to map several fields
  8479. function SetOptions(aOptions: TSQLRecordPropertiesMappingOptions): PSQLRecordPropertiesMapping;
  8480. /// add several custom field mappings
  8481. // - can be used e.g. as
  8482. // ! aModel.Props[TSQLMyExternal].ExternalDB.
  8483. // ! MapFields(['IntField1','ExtField1', 'IntField2','ExtField2']);
  8484. // - will re-compute all needed SQL statements as needed, and initialize
  8485. // fSortedFieldsName[] and fSortedFieldsIndex[] internal sorted arrays
  8486. // - is slightly faster than several chained MapField() calls, since SQL
  8487. // will be computed only once
  8488. function MapFields(const InternalExternalPairs: array of RawUTF8): PSQLRecordPropertiesMapping;
  8489. public
  8490. /// initialize the field mapping for a given TSQLRecord
  8491. // - if AutoComputeSQL is true, will pre-compute all needed SQL from the
  8492. // supplied information
  8493. // - will left void fSortedFieldsName[] and fSortedFieldsIndex[], to disable
  8494. // custom field mapping
  8495. procedure Init(Table: TSQLRecordClass; const MappedTableName: RawUTF8;
  8496. MappedConnection: TObject; AutoComputeSQL: boolean); overload;
  8497. /// map a field name from its internal name to its external name
  8498. // - raise an EORMException if the supplied field name is not defined in
  8499. // the TSQLRecord as ID or a published property
  8500. function InternalToExternal(const FieldName: RawUTF8): RawUTF8;
  8501. /// map a CSV list of field names from its internals to its externals values
  8502. // - raise an EORMException if any of the supplied field name is not defined
  8503. // in the TSQLRecord as ID or as property (RowIDFieldName or FieldNames[])
  8504. // - to be used for a simple CSV (e.g. for INSERT/SELECT statements):
  8505. // ! ExtCSV := InternalCSVToExternalCSV('ID,Name');
  8506. // - or for a more complex CSV (e.g. for UPDATE statements);
  8507. // ! ExtCSV := InternalCSVToExternalCSV('ID=?,Name=?','=?,'=?');
  8508. function InternalCSVToExternalCSV(const CSVFieldNames: RawUTF8;
  8509. const Sep: RawUTF8=','; const SepEnd: RawUTF8=''): RawUTF8;
  8510. /// create a list of external field names, from the internal field names
  8511. // - raise an EORMException if any of the supplied field name is not defined
  8512. // in the TSQLRecord as ID or a published property
  8513. // - if IntFieldIndex is set, it will store an array of internal field
  8514. // indexes, i.e. -1 for ID or index in in FieldNames[] for other fields
  8515. procedure InternalToExternalDynArray(const IntFieldNames: array of RawUTF8;
  8516. out result: TRawUTF8DynArray; IntFieldIndex: PIntegerDynArray=nil);
  8517. /// map an external field name into its internal field name
  8518. // - return '' if the external field name is not RowIDFieldName nor in
  8519. // FieldNames[]
  8520. function ExternalToInternalOrNull(const ExtFieldName: RawUTF8): RawUTF8;
  8521. /// map an external field name into its internal field index
  8522. // - returns the index >=0 in FieldNames[] for a matching external field
  8523. // - returns -1 if the field name is RowIDFieldName
  8524. // - returns -2 if the field name is not mapped
  8525. function ExternalToInternalIndex(const ExtFieldName: RawUTF8): integer;
  8526. /// append a field name to a RawUTF8 Text buffer
  8527. // - if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN (-1), appends RowIDFieldName
  8528. // - on error (i.e. if FieldIndex is out of range) will return TRUE
  8529. // - otherwise, will return FALSE and append the external field name to Text
  8530. function AppendFieldName(FieldIndex: Integer; var Text: RawUTF8): boolean;
  8531. /// return the field name as RawUTF8 value
  8532. // - if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN (-1), appends RowIDFieldName
  8533. // - otherwise, will return the external field name
  8534. function FieldNameByIndex(FieldIndex: Integer): RawUTF8;
  8535. /// opaque object used on the Server side to specify e.g. the DB connection
  8536. // - will define such a generic TObject, to avoid any unecessary type
  8537. // dependency to other units, e.g. the SynDB unit in mORMot.pas
  8538. // - in practice, will be assigned by VirtualTableExternalRegister() to
  8539. // a TSQLDBConnectionProperties instance in mORMotDB.pas, or by
  8540. // StaticMongoDBRegister() to a TMongoCollection instance, or by
  8541. // TDDDRepositoryRestObjectMapping.Create to its associated TSQLRest
  8542. // - in ORM context, equals nil if the table is internal to SQLite3:
  8543. // ! if Server.Model.Props[TSQLArticle].ExternalDB.ConnectionProperties=nil then
  8544. // ! // this is not an external table, since Init() was not called
  8545. property ConnectionProperties: TObject read fConnectionProperties;
  8546. /// the associated TSQLRecordProperties
  8547. property Properties: TSQLRecordProperties read fProps;
  8548. /// used on the Server side to specify the external DB table name
  8549. // - e.g. for including a schema name or an existing table name, with an
  8550. // OleDB/MSSQL/Oracle/MySQL/PostgreSQL/Jet/SQLite3 backend
  8551. // - equals SQLTableName by default (may be overridden e.g. by mORMotDB's
  8552. // VirtualTableExternalRegister procedure)
  8553. property TableName: RawUTF8 read fTableName;
  8554. /// pre-computed SQL statements for this external TSQLRecord in this model
  8555. // - you can use those SQL statements directly with the external engine
  8556. // - filled if AutoComputeSQL was set to true in Init() method
  8557. property SQL: TSQLModelRecordPropertiesSQL read fSQL;
  8558. /// the ID/RowID customized external field name, if any
  8559. // - is 'ID' by default, since 'RowID' is a reserved column name for some
  8560. // database engines (e.g. Oracle)
  8561. // - can be customized e.g. via
  8562. // ! aModel.Props[TSQLMyExternal].ExternalDB.MapField('ID','ExternalID');
  8563. property RowIDFieldName: RawUTF8 read fRowIDFieldName;
  8564. /// the external field names, following fProps.Props.Field[] order
  8565. // - excluding ID/RowID field, which is stored in RowIDFieldName
  8566. property FieldNames: TRawUTF8DynArray read fFieldNames;
  8567. /// each bit set, following fProps.Props.Field[]+1 order (i.e. 0=ID,
  8568. // 1=Field[0], ...), indicates that this external field name
  8569. // has not been mapped
  8570. property FieldNamesMatchInternal: TSQLFieldBits read fFieldNamesMatchInternal;
  8571. /// how the mapping process would take place
  8572. property Options: TSQLRecordPropertiesMappingOptions read fOptions;
  8573. /// each time MapField/MapFields is called, this number will increase
  8574. // - can be used to track mapping changes in real time
  8575. property MappingVersion: cardinal read fMappingVersion;
  8576. end;
  8577. /// dynamic array of TSQLModelRecordProperties
  8578. // - used by TSQLModel to store the non-shared information of all its tables
  8579. TSQLModelRecordPropertiesObjArray = array of TSQLModelRecordProperties;
  8580. /// ORM properties associated to a TSQLRecord within a given model
  8581. // - "stable" / common properties derivated from RTTI are shared in the
  8582. // TSQLRecordProperties instance
  8583. // - since the same TSQLRecord can be defined in several models, with diverse
  8584. // implementation patterns (e.g. internal in one, external in another),
  8585. // this class is used to regroup all model-specific settings, like SQL
  8586. // pre-generated patterns or external DB properties
  8587. TSQLModelRecordProperties = class
  8588. protected
  8589. fProps: TSQLRecordProperties;
  8590. fKind: TSQLRecordVirtualKind;
  8591. fModel: TSQLModel;
  8592. fTableIndex: integer;
  8593. fFTSWithoutContentTableIndex: integer;
  8594. fFTSWithoutContentFields: RawUTF8;
  8595. procedure SetKind(Value: TSQLRecordVirtualKind);
  8596. function GetProp(const PropName: RawUTF8): TSQLPropInfo;
  8597. public
  8598. /// pre-computed SQL statements for this TSQLRecord in this model
  8599. // - those statements will work for internal tables, not for external
  8600. // tables with mapped table or fields names
  8601. SQL: TSQLModelRecordPropertiesSQL;
  8602. /// allow SQL process for one external TSQLRecord in this model
  8603. ExternalDB: TSQLRecordPropertiesMapping;
  8604. /// initialize the ORM properties from the TSQLRecord RTTI and the supplied
  8605. // TSQLModel
  8606. constructor Create(aModel: TSQLModel; aTable: TSQLRecordClass; aKind: TSQLRecordVirtualKind);
  8607. /// clone ORM properties from an existing TSQLModelRecordProperties to
  8608. // another model
  8609. constructor CreateFrom(aModel: TSQLModel; aSource: TSQLModelRecordProperties);
  8610. /// compute the SQL statement to be executed for a specific SELECT
  8611. // - non simple fields (e.g. BLOBs) will be excluded if SelectFields='*'
  8612. // - by default, will return the SELECT statement to be used for internal
  8613. // virtual SQLite3 table - but if ExternalTable is TRUE, then it will
  8614. // compute a SELECT matching ExternalDB settings
  8615. function SQLFromSelectWhere(const SelectFields, Where: RawUTF8): RawUTF8;
  8616. /// define if a FTS4 virtual table will not store its content, but would
  8617. // be defined as an "external content" FTS4 table
  8618. // - see https://www.sqlite.org/fts3.html#section_6_2_2
  8619. // - the virtual table will be created with content="ContentTableName",
  8620. // and all fields of the FTS4 table
  8621. // - by design, all fields of the FTS4 table should exist in the source
  8622. // ContentTable - otherwise an exception is raised
  8623. // - the indexed text will be assigned to the FTS4 table, using triggers
  8624. // generated by TSQLRecordFTS4.InitializeTable at table creation
  8625. // - note that FTS3 does not support this feature
  8626. procedure FTS4WithoutContent(ContentTable: TSQLRecordClass);
  8627. /// the table index of this TSQLRecord in the associated Model
  8628. property TableIndex: Integer read fTableIndex;
  8629. /// direct access to a property RTTI information, by name
  8630. property Prop[const PropName: RawUTF8]: TSQLPropInfo read GetProp; default;
  8631. published
  8632. /// the shared TSQLRecordProperties information of this TSQLRecord
  8633. // - as retrieved from RTTI
  8634. property Props: TSQLRecordProperties read fProps;
  8635. /// define if is a normal table (rSQLite3), an FTS3/FTS4/R-Tree virtual
  8636. // table or a custom TSQLVirtualTable*ID (rCustomForcedID/rCustomAutoID)
  8637. // - when set, all internal SQL statements will be (re)created, depending of
  8638. // the expected ID/RowID column name expected (i.e. SQLTableSimpleFields[]
  8639. // and SQLSelectAll[] - SQLUpdateSet and SQLInsertSet do not include ID)
  8640. property Kind: TSQLRecordVirtualKind read fKind write SetKind default rSQLite3;
  8641. end;
  8642. /// how a TSQLModel stores a foreign link to be cascaded
  8643. TSQLModelRecordReference = record
  8644. TableIndex: integer;
  8645. FieldType: TSQLPropInfo;
  8646. FieldTable: TSQLRecordClass;
  8647. FieldTableIndex: integer;
  8648. CascadeDelete: boolean;
  8649. end;
  8650. PSQLModelRecordReference = ^TSQLModelRecordReference;
  8651. /// a Database Model (in a MVC-driven way), for storing some tables types
  8652. // as TSQLRecord classes
  8653. // - share this Model between TSQLRest Client and Server
  8654. // - use this class to access the table properties: do not rely on the
  8655. // low-level database methods (e.g. TSQLDataBase.GetTableNames), since the
  8656. // tables may not exist in the main SQLite3 database, but in-memory or external
  8657. // - don't modify the order of Tables inside this Model, if you publish
  8658. // some TRecordReference property in any of your tables
  8659. TSQLModel = class
  8660. private
  8661. fTables: TSQLRecordClassDynArray;
  8662. fRoot: RawUTF8;
  8663. fRootUpper: RawUTF8;
  8664. fTablesMax: integer;
  8665. fActions: PEnumType;
  8666. fEvents: PEnumType;
  8667. fTableProps: TSQLModelRecordPropertiesObjArray;
  8668. fCustomCollationForAll: array[TSQLFieldType] of RawUTF8;
  8669. {$ifndef LVCL}
  8670. fOnClientIdle: TOnIdleSynBackgroundThread;
  8671. {$endif}
  8672. /// contains the caller of CreateOwnedStream()
  8673. fRestOwner: TSQLRest;
  8674. /// for every table, contains a locked record list
  8675. // - very fast, thanks to the use of a dynamic array with one entry by table
  8676. fLocks: TSQLLocksDynArray;
  8677. /// for fastest SQL Table name lookup via binary search
  8678. fSortedTablesName: TRawUTF8DynArray;
  8679. fSortedTablesNameIndex: TIntegerDynArray;
  8680. /// will contain the registered virtual table modules
  8681. fVirtualTableModule: array of TSQLVirtualTableClass;
  8682. /// this array contain all TRecordReference and TSQLRecord properties
  8683. // existing in the database model
  8684. // - used in TSQLRestServer.Delete() to enforce relational database coherency
  8685. // after deletion of a record: all other records pointing to it will be
  8686. // reset to 0 or deleted (if CascadeDelete is true) by
  8687. // TSQLRestServer.AfterDeleteForceCoherency
  8688. fRecordReferences: array of TSQLModelRecordReference;
  8689. fIDGenerator: array of TSynUniqueIdentifierGenerator;
  8690. procedure SetTableProps(aIndex: integer);
  8691. function GetTableIndexSafe(aTable: TSQLRecordClass;
  8692. RaiseExceptionIfNotExisting: boolean): integer;
  8693. function GetTableProps(aClass: TSQLRecordClass): TSQLModelRecordProperties;
  8694. /// get the enumerate type information about the possible actions to be
  8695. function GetLocks(aTable: TSQLRecordClass): PSQLLocks;
  8696. function GetTable(const SQLTableName: RawUTF8): TSQLRecordClass;
  8697. function GetTableExactIndex(const TableName: RawUTF8): integer;
  8698. function GetTableExactClass(const TableName: RawUTF8): TSQLRecordClass;
  8699. function getURI(aTable: TSQLRecordClass): RawUTF8;
  8700. function getURIID(aTable: TSQLRecordClass; aID: TID): RawUTF8;
  8701. function getURICallBack(const aMethodName: RawUTF8; aTable: TSQLRecordClass; aID: TID): RawUTF8;
  8702. public
  8703. /// initialize the Database Model
  8704. // - set the Tables to be associated with this Model, as TSQLRecord classes
  8705. // - set the optional Root URI path of this Model
  8706. // - initialize the fIsUnique[] array from "stored AS_UNIQUE" (i.e. "stored
  8707. // false") published properties of every TSQLRecordClass
  8708. constructor Create(const Tables: array of TSQLRecordClass; const aRoot: RawUTF8='root'); reintroduce; overload;
  8709. /// you should not use this constructor, but one of the overloaded versions,
  8710. // specifying the associated TSQLRecordClass
  8711. constructor Create; reintroduce; overload;
  8712. /// clone an existing Database Model
  8713. // - all supplied classes won't be redefined as non-virtual:
  8714. // VirtualTableExternalRegister explicit calls are not mandatory here
  8715. constructor Create(CloneFrom: TSQLModel); reintroduce; overload;
  8716. /// initialize the Database Model from an User Interface parameter structure
  8717. // - this constructor will reset all supplied classes to be defined as
  8718. // non-virtual (i.e. Kind=rSQLite3): VirtualTableExternalRegister explicit
  8719. // calls are to be made if tables should be managed as external
  8720. constructor Create(Owner: TSQLRest; TabParameters: PSQLRibbonTabParameters;
  8721. TabParametersCount, TabParametersSize: integer;
  8722. const NonVisibleTables: array of TSQLRecordClass;
  8723. Actions: PTypeInfo=nil; Events: PTypeInfo=nil;
  8724. const aRoot: RawUTF8='root'); reintroduce; overload;
  8725. /// release associated memory
  8726. destructor Destroy; override;
  8727. /// add the class if it doesn't exist yet
  8728. // - return index in Tables[] if not existing yet and successfully added (in this case,
  8729. // aTableIndexCreated^ is set to the newly created index in Tables[])
  8730. // - supplied class will be redefined as non-virtual: VirtualTableExternalRegister
  8731. // explicit call is to be made if table should be managed as external
  8732. // - return FALSE if already present, or TRUE if was added to the internal list
  8733. function AddTable(aTable: TSQLRecordClass; aTableIndexCreated: PInteger=nil): boolean;
  8734. /// add the class if it doesn't exist yet as itself or as inherited class
  8735. // - similar to AddTable(), but any class inheriting from the supplied type
  8736. // would be considered as sufficient
  8737. // - return the class which has been added, or was already there as
  8738. // inherited, so that could be used for further instance creation:
  8739. // ! fSQLAuthUserClass := Model.AddTableInherited(TSQLAuthUser);
  8740. function AddTableInherited(aTable: TSQLRecordClass): pointer;
  8741. /// get the index of aTable in Tables[]
  8742. // - returns -1 if the table is not in the model
  8743. function GetTableIndex(aTable: TSQLRecordClass): integer; overload;
  8744. /// get the index of any class inherithing from aTable in Tables[]
  8745. // - returns -1 if no table is matching in the model
  8746. function GetTableIndexInheritsFrom(aTable: TSQLRecordClass): integer;
  8747. /// get the index of aTable in Tables[]
  8748. // - raise an EModelException if the table is not in the model
  8749. function GetTableIndexExisting(aTable: TSQLRecordClass): integer;
  8750. /// get the index of a table in Tables[]
  8751. // - expects SQLTableName to be SQL-like formatted (i.e. without TSQL[Record])
  8752. function GetTableIndex(const SQLTableName: RawUTF8): integer; overload;
  8753. /// get the index of a table in Tables[]
  8754. // - expects SQLTableName to be SQL-like formatted (i.e. without TSQL[Record])
  8755. function GetTableIndex(SQLTableName: PUTF8Char): integer; overload;
  8756. /// return the UTF-8 encoded SQL source to create the table
  8757. function GetSQLCreate(aTableIndex: integer): RawUTF8;
  8758. /// return the UTF-8 encoded SQL source to add the corresponding field
  8759. // via a "ALTER TABLE" statement
  8760. function GetSQLAddField(aTableIndex, aFieldIndex: integer): RawUTF8;
  8761. /// return the TRecordReference pointing to the specified record
  8762. function RecordReference(Table: TSQLRecordClass; ID: TID): TRecordReference;
  8763. /// return the table class correspondig to a TRecordReference
  8764. function RecordReferenceTable(const Ref: TRecordReference): TSQLRecordClass;
  8765. /// return TRUE if the specified field of this class was marked as unique
  8766. // - an unique field is defined as "stored AS_UNIQUE" (i.e. "stored false")
  8767. // in its property definition
  8768. // - reflects the internal private fIsUnique propery
  8769. function GetIsUnique(aTable: TSQLRecordClass; aFieldIndex: integer): boolean;
  8770. /// try to retrieve a table index from a SQL statement
  8771. // - naive search of '... FROM TableName' pattern in the supplied SQL,
  8772. // using GetTableNameFromSQLSelect() function
  8773. // - if EnsureUniqueTableInFrom is TRUE, it will check that only one Table
  8774. // is in the FROM clause, otherwise it will return the first Table specified
  8775. function GetTableIndexFromSQLSelect(const SQL: RawUTF8; EnsureUniqueTableInFrom: boolean): integer;
  8776. /// try to retrieve one or several table index from a SQL statement
  8777. // - naive search of '... FROM Table1,Table2' pattern in the supplied SQL,
  8778. // using GetTableNamesFromSQLSelect() function
  8779. function GetTableIndexesFromSQLSelect(const SQL: RawUTF8): TIntegerDynArray;
  8780. /// try to retrieve one or several TSQLRecordClass from a SQL statement
  8781. // - naive search of '... FROM Table1,Table2' pattern in the supplied SQL,
  8782. // using GetTableNamesFromSQLSelect() function
  8783. function GetTablesFromSQLSelect(const SQL: RawUTF8): TSQLRecordClassDynArray;
  8784. /// check if the supplied URI matches the model's Root property
  8785. // - allows sub-domains, e.g. if Root='root/sub1', then '/root/sub1/toto' and
  8786. // '/root/sub1?n=1' will match, whereas '/root/sub1nope/toto' won't
  8787. // - the returned enumerates allow to check if the match was exact (e.g.
  8788. // 'root/sub' matches exactly Root='root'), or with character case
  8789. // approximation (e.g. 'Root/sub' approximates Root='root')
  8790. function URIMatch(const URI: RawUTF8): TSQLRestModelMatch;
  8791. /// compute the SQL statement to be executed for a specific SELECT on Tables
  8792. // - you can set multiple Table class in Tables: the statement will contain the
  8793. // table name ('SELECT T1.F1,T1.F2,T1.F3,T2.F1,T2.F2 FROM T1,T2 WHERE ..' e.g.)
  8794. function SQLFromSelectWhere(const Tables: array of TSQLRecordClass;
  8795. const SQLSelect, SQLWhere: RawUTF8): RawUTF8;
  8796. /// set a custom SQlite3 text column collation for all fields of a given
  8797. // type for all TSQLRecord of this model
  8798. // - can be used e.g. to override ALL default COLLATE SYSTEMNOCASE of RawUTF8,
  8799. // or COLLATE ISO8601 for TDateTime, and let the generated SQLite3 file be
  8800. // available outside the scope of mORMot's SQLite3 engine
  8801. // - collations defined within our SynSQLite3 unit are named BINARY, NOCASE,
  8802. // RTRIM and our custom SYSTEMNOCASE, ISO8601, WIN32CASE, WIN32NOCASE: if
  8803. // you want to use the slow but Unicode ready Windows API, set for each model:
  8804. // ! SetCustomCollationForAll(sftUTF8Text,'WIN32CASE');
  8805. // - shall be set on both Client and Server sides, otherwise some issues
  8806. // may occur
  8807. procedure SetCustomCollationForAll(aFieldType: TSQLFieldType;
  8808. const aCollationName: RawUTF8);
  8809. /// allow to validate length of all text published properties of all tables
  8810. // of this model
  8811. // - the "index" attribute of the RawUTF8/string published properties could
  8812. // be used to specify a maximum length for external VARCHAR() columns
  8813. // - SQLite3 will just ignore this "index" information, but it could be
  8814. // handy to be able to validate the value length before sending to the DB
  8815. // - this method will create TSynValidateText corresponding to the maximum
  8816. // field size specified by the "index" attribute, to validate before write
  8817. // - will expect the "index" value to be in UTF-16 codepoints, unless
  8818. // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length
  8819. procedure SetMaxLengthValidatorForAllTextFields(IndexIsUTF8Length: boolean=false);
  8820. /// allow to filter the length of all text published properties of all tables
  8821. // of this model
  8822. // - the "index" attribute of the RawUTF8/string published properties could
  8823. // be used to specify a maximum length for external VARCHAR() columns
  8824. // - SQLite3 will just ignore this "index" information, but it could be
  8825. // handy to be able to filter the value length before sending to the DB
  8826. // - this method will create TSynFilterTruncate corresponding to the maximum
  8827. // field size specified by the "index" attribute, to validate before write
  8828. // - will expect the "index" value to be in UTF-16 codepoints, unless
  8829. // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length
  8830. procedure SetMaxLengthFilterForAllTextFields(IndexIsUTF8Length: boolean=false);
  8831. {$ifndef NOVARIANTS}
  8832. /// customize the TDocVariant options for all variant published properties
  8833. // - will change the TSQLPropInfoRTTIVariant.DocVariantOptions value
  8834. // - use e.g. as SetVariantFieldDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED)
  8835. procedure SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions);
  8836. {$endif}
  8837. /// force a given table to use a TSynUniqueIdentifierGenerator for its IDs
  8838. /// - would initialize a generator for the supplied table, using the
  8839. // given 16-bit process identifier
  8840. // - you can supply an obfuscation key, which should be shared for the
  8841. // whole system, so that you may use FromObfuscated/ToObfuscated methods
  8842. function SetIDGenerator(aTable: TSQLRecordClass;
  8843. aIdentifier: TSynUniqueIdentifierProcess; const aSharedObfuscationKey: RawUTF8=''): TSynUniqueIdentifierGenerator;
  8844. /// returns the TSynUniqueIdentifierGenerator associated to a table, if any
  8845. function GetIDGenerator(aTable: TSQLRecordClass): TSynUniqueIdentifierGenerator;
  8846. /// assign an enumeration type to the possible actions to be performed
  8847. // with this model
  8848. // - call with the TypeInfo() pointer result of an enumeration type
  8849. // - actions are handled by TSQLRecordForList in the mORMotToolBar.pas unit
  8850. procedure SetActions(aActions: PTypeInfo);
  8851. /// assign an enumeration type to the possible events to be triggered
  8852. // with this class model
  8853. // - call with the TypeInfo() pointer result of an enumeration type
  8854. procedure SetEvents(aEvents: PTypeInfo);
  8855. /// get the text conversion of a given Action, ready to be displayed
  8856. function ActionName(const Action): string;
  8857. /// get the text conversion of a given Event, ready to be displayed
  8858. function EventName(const Event): string;
  8859. /// register a Virtual Table module for a specified class
  8860. // - to be called server-side only (Client don't need to know the virtual
  8861. // table implementation details, and it will increase the code size)
  8862. // - aClass parameter could be either a TSQLRecordVirtual class, either
  8863. // a TSQLRecord class which has its kind set to rCustomForcedID or
  8864. // rCustomAutoID (e.g. TSQLRecordMany calling VirtualTableExternalRegister)
  8865. // - optional aExternalTableName and aExternalDataBase can be used to
  8866. // specify e.g. connection parameters as expected by mORMotDB
  8867. // - call it before TSQLRestServer.Create()
  8868. function VirtualTableRegister(aClass: TSQLRecordClass;
  8869. aModule: TSQLVirtualTableClass; const aExternalTableName: RawUTF8='';
  8870. aExternalDataBase: TObject=nil): boolean;
  8871. /// retrieve a Virtual Table module associated to a class
  8872. function VirtualTableModule(aClass: TSQLRecordClass): TSQLVirtualTableClass;
  8873. /// create a New TSQLRecord instance for a specific Table
  8874. // - expects SQLTableName to be SQL-like formated (i.e. without TSQL[Record])
  8875. // - use this to create a working copy of a table's record, e.g.
  8876. // - don't forget to Free it when not used any more (use a try...finally
  8877. // block)
  8878. // - it's prefered in practice to directly call TSQLRecord*.Create()
  8879. // in your code
  8880. function NewRecord(const SQLTableName: RawUTF8): TSQLRecord;
  8881. /// lock a record
  8882. // - returns true on success, false if was already locked
  8883. function Lock(aTable: TSQLRecordClass; aID: TID): boolean; overload;
  8884. /// lock a record
  8885. // - returns true on success, false if was already locked
  8886. function Lock(aTableIndex, aID: TID): boolean; overload;
  8887. /// lock a record
  8888. // - returns true on success, false if was already locked
  8889. function Lock(aRec: TSQLRecord): boolean; overload;
  8890. /// unlock a specified record
  8891. // - returns true on success, false if was not already locked
  8892. function UnLock(aTable: TSQLRecordClass; aID: TID): boolean; overload;
  8893. /// unlock a specified record
  8894. // - returns true on success, false if was not already locked
  8895. function UnLock(aTableIndex: integer; aID: TID): boolean; overload;
  8896. /// unlock a specified record
  8897. // - returns true on success, false if was not already locked
  8898. function UnLock(aRec: TSQLRecord): boolean; overload;
  8899. /// unlock all previously locked records
  8900. procedure UnLockAll;
  8901. /// return true if a specified record is locked
  8902. function isLocked(aTable: TSQLRecordClass; aID: TID): boolean; overload;
  8903. /// return true if a specified record is locked
  8904. function isLocked(aRec: TSQLRecord): boolean; overload;
  8905. /// delete all the locked IDs entries, after a specified time
  8906. // - to be used to release locked records if the client crashed
  8907. // - default value is 30 minutes, which seems correct for common usage
  8908. procedure PurgeOlderThan(MinutesFromNow: cardinal=30);
  8909. /// get the classes list (TSQLRecord descendent) of all available tables
  8910. property Tables: TSQLRecordClassDynArray read fTables;
  8911. /// get a class from a table name
  8912. // - expects SQLTableName to be SQL-like formated (i.e. without TSQL[Record])
  8913. property Table[const SQLTableName: RawUTF8]: TSQLRecordClass read GetTable; default;
  8914. /// get a class from a table TableName (don't truncate TSQLRecord* if necessary)
  8915. property TableExact[const TableName: RawUTF8]: TSQLRecordClass read GetTableExactClass;
  8916. /// get the URI for a class in this Model, as 'ModelRoot/SQLTableName'
  8917. property URI[aClass: TSQLRecordClass]: RawUTF8 read getURI;
  8918. /// the associated ORM information for a given TSQLRecord class
  8919. // - raise an EModelException if aClass is not declared within this model
  8920. // - returns the corresponding TableProps[] item if the class is known
  8921. property Props[aClass: TSQLRecordClass]: TSQLModelRecordProperties read GetTableProps;
  8922. /// the maximum index of TableProps[] class properties array
  8923. property TablesMax: integer read fTablesMax;
  8924. // performed with this model
  8925. // - Actions are e.g. linked to some buttons in the User Interface
  8926. property Actions: PEnumType read fActions;
  8927. /// get the enumerate type information about the possible Events to be
  8928. // performed with this model
  8929. // - Events can be linked to actions and custom status, to provide a
  8930. // centralized handling of logging (e.g. in an Audit Trail table)
  8931. property Events: PEnumType read fEvents;
  8932. /// this property value is used to auto free the database Model class
  8933. // - set this property after Owner.Create() in order to have
  8934. // Owner.Destroy autofreeing it
  8935. property Owner: TSQLRest read fRestOwner write fRestOwner;
  8936. /// for every table, contains a locked record list
  8937. // - very fast, thanks to the use one TSQLLocks entry by table
  8938. property Locks: TSQLLocksDynArray read fLocks;
  8939. {$ifndef LVCL}
  8940. /// set a callback event to be executed in loop during client remote
  8941. // blocking process, e.g. to refresh the UI during a somewhat long request
  8942. // - will be passed to TSQLRestClientURI.OnIdle property by
  8943. // TSQLRestClientURI.RegisteredClassCreateFrom() method, if applying
  8944. property OnClientIdle: TOnIdleSynBackgroundThread
  8945. read fOnClientIdle write fOnClientIdle;
  8946. {$endif}
  8947. published
  8948. /// the Root URI path of this Database Model
  8949. property Root: RawUTF8 read fRoot write fRoot;
  8950. /// the associated ORM information about all handled TSQLRecord class properties
  8951. // - this TableProps[] array will map the Tables[] array, and will allow
  8952. // fast direct access to the Tables[].RecordProps values
  8953. property TableProps: TSQLModelRecordPropertiesObjArray read fTableProps;
  8954. end;
  8955. PRecordRef = ^RecordRef;
  8956. /// useful object to type cast TRecordReference type value into explicit
  8957. // TSQLRecordClass and ID
  8958. // - use RecordRef(Reference).TableIndex/Table/ID/Text methods to retrieve
  8959. // the details of a TRecordReference encoded value
  8960. // - use TSQLRest.Retrieve(Reference) to get a record content from DB
  8961. // - instead of From(Reference).From(), you could use the more explicit
  8962. // TSQLRecord.RecordReference(Model) or TSQLModel.RecordReference()
  8963. // methods or RecordReference() function to encode the value
  8964. // - don't change associated TSQLModel tables order, since TRecordReference
  8965. // depends on it to store the Table type
  8966. // - since 6 bits are used for the table index, the corresponding table
  8967. // MUST appear in the first 64 items of the associated TSQLModel.Tables[]
  8968. RecordRef = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  8969. public
  8970. /// the value itself
  8971. // - (value and 63) is the TableIndex in the current database Model
  8972. // - (value shr 6) is the ID of the record in this table
  8973. // - value=0 means no reference stored
  8974. // - we use this coding and not the opposite (Table in MSB) to minimize
  8975. // integer values; but special UTF8CompareRecord() function has to be used
  8976. // for sorting
  8977. // - type definition matches TRecordReference (i.e. Int64/TID) to allow
  8978. // typecast as such:
  8979. // ! aClass := PRecordRef(@Reference)^.Table(Model);
  8980. Value: TID;
  8981. /// return the index of the content Table in the TSQLModel
  8982. function TableIndex: integer; {$ifdef HASINLINE}inline;{$endif}
  8983. /// return the class of the content in a specified TSQLModel
  8984. function Table(Model: TSQLModel): TSQLRecordClass;
  8985. /// return the ID of the content
  8986. function ID: TID; {$ifdef HASINLINE}inline;{$endif}
  8987. /// fill Value with the corresponding parameters
  8988. // - since 6 bits are used for the table index, aTable MUST appear in the
  8989. // first 64 items of the associated TSQLModel.Tables[] array
  8990. procedure From(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID);
  8991. /// get a ready to be displayed text from the stored Table and ID
  8992. // - display 'Record 2301' e.g.
  8993. function Text(Model: TSQLModel): RawUTF8; overload;
  8994. /// get a ready to be displayed text from the stored Table and ID
  8995. // - display 'Record "RecordName"' e.g.
  8996. function Text(Rest: TSQLRest): RawUTF8; overload;
  8997. end;
  8998. /// this kind of record array can be used for direct coordinates storage
  8999. TSQLRecordTreeCoords = array[0..RTREE_MAX_DIMENSION-1] of packed record
  9000. min, max: double; end;
  9001. /// a base record, corresponding to an R-Tree table
  9002. // - an R-Tree is a special index that is designed for doing range queries.
  9003. // R-Trees are most commonly used in geospatial systems where each entry is a
  9004. // rectangle with minimum and maximum X and Y coordinates. Given a query
  9005. // rectangle, an R-Tree is able to quickly find all entries that are contained
  9006. // within the query rectangle or which overlap the query rectangle. This idea
  9007. // is easily extended to three dimensions for use in CAD systems. R-Trees also
  9008. // find use in time-domain range look-ups. For example, suppose a database
  9009. // records the starting and ending times for a large number of events. A R-Tree
  9010. // is able to quickly find all events, for example, that were active at any
  9011. // time during a given time interval, or all events that started during a
  9012. // particular time interval, or all events that both started and ended within
  9013. // a given time interval. And so forth. See http:// www.sqlite.org/rtree.html
  9014. // - any record which inherits from this class must have only sftFloat
  9015. // (double) fields, grouped by pairs, each as minimum- and maximum-value,
  9016. // up to 5 dimensions (i.e. 11 columns, including the ID property)
  9017. // - the ID: TID property must be set before adding a TSQLRecordRTree to
  9018. // the database, e.g. to link a R-Tree representation to a regular
  9019. // TSQLRecord table
  9020. // - queries against the ID or the coordinate ranges are almost immediate: so
  9021. // you can e.g. extract some coordinates box from the regular TSQLRecord
  9022. // table, then use a TSQLRecordRTree joined query to make the process faster;
  9023. // this is exactly what the TSQLRestClient.RTreeMatch method offers
  9024. TSQLRecordRTree = class(TSQLRecordVirtual)
  9025. public
  9026. { override this class function to implement a custom box coordinates
  9027. from a given BLOB content
  9028. - by default, the BLOB array will contain a simple array of double
  9029. - but you can override this method to handle a custom BLOB field content,
  9030. intended to hold some kind of binary representation of the precise
  9031. boundaries of the object, and convert it into box coordinates as
  9032. understood by the ContainedIn() class function
  9033. - the number of pairs in OutCoord will be taken from the current number
  9034. of published double properties
  9035. - used e.g. by the TSQLRestClient.RTreeMatch method }
  9036. class procedure BlobToCoord(const InBlob; var OutCoord: TSQLRecordTreeCoords); virtual;
  9037. { override this class function to implement a custom SQL *_in() function
  9038. - by default, the BLOB array will be decoded via the BlobToCoord class
  9039. procedure, and will create a SQL function from the class name
  9040. - for instance, the following class will define a 2 dimensional
  9041. MapBox_in() function
  9042. ! TSQLRecordMapBox = class(TSQLRecordRTree)
  9043. ! protected
  9044. ! fMinX, fMaxX, fMinY, fMaxY: double;
  9045. ! published
  9046. ! property MinX: double read fMinX write fMinX;
  9047. ! property MaxX: double read fMaxX write fMaxX;
  9048. ! property MinY: double read fMinY write fMinY;
  9049. ! property MaxY: double read fMaxY write fMaxY;
  9050. ! end;
  9051. - used e.g. by the TSQLRestClient.RTreeMatch method }
  9052. class function ContainedIn(const BlobA,BlobB): boolean; virtual;
  9053. /// will return 'MapBox_in' e.g. for TSQLRecordMapBox
  9054. class function RTreeSQLFunctionName: RawUTF8;
  9055. end;
  9056. /// a base record, corresponding to a FTS3 table, i.e. implementing full-text
  9057. // - FTS3/FTS4 table are SQLite virtual tables which allow users to perform
  9058. // full-text searches on a set of documents. The most common (and effective)
  9059. // way to describe full-text searches is "what Google, Yahoo and Altavista do
  9060. // with documents placed on the World Wide Web". Users input a term, or
  9061. // series of terms, perhaps connected by a binary operator or grouped together
  9062. // into a phrase, and the full-text query system finds the set of documents
  9063. // that best matches those terms considering the operators and groupings the
  9064. // user has specified. See http:// sqlite.org/fts3.html
  9065. // - any record which inherits from this class must have only sftUTF8Text
  9066. // (RawUTF8) fields - with Delphi 2009+, you can have string fields
  9067. // - this record has its fID: TID property which may be published
  9068. // as DocID, to be consistent with SQLite3 praxis, and reflect that it
  9069. // points to an ID of another associated TSQLRecord
  9070. // - a good approach is to store your data in a regular TSQLRecord table, then
  9071. // store your text content in a separated FTS3 table, associated to this
  9072. // TSQLRecordFTS3 table via its ID/DocID
  9073. // - the ID/DocID property can be set when the record is added, to retrieve any
  9074. // associated TSQLRecord (note that for a TSQLRecord record,
  9075. // the ID property can't be set at adding, but is calculated by the engine)
  9076. // - static tables don't handle TSQLRecordFTS3 classes
  9077. // - by default, the FTS3 engine ignore all characters >= #80, but handle
  9078. // low-level case insentivity (i.e. 'A'..'Z') so you must keep your
  9079. // request with the same range for upper case
  9080. // - by default, the "simple" tokenizer is used, but you can inherits from
  9081. // TSQLRecordFTS3Porter class if you want a better English matching, using
  9082. // the Porter Stemming algorithm, or TSQLRecordFTS3Unicode61 for Unicode
  9083. // support - see http:// sqlite.org/fts3.html#tokenizer
  9084. // - you can select either the FTS3 engine, or the more efficient (and new)
  9085. // FTS4 engine (available since version 3.7.4), by using the TSQLRecordFTS4 type
  9086. // - in order to make FTS3/FTS4 queries, use the dedicated TSQLRest.FTSMatch
  9087. // method, with the MATCH operator (you can use regular queries, but you must
  9088. // specify 'RowID' instead of 'DocID' or 'ID' because of FTS3 Virtual
  9089. // table specificity):
  9090. // ! var IDs: TIDDynArray;
  9091. // ! if FTSMatch(TSQLMyFTS3Table,'text MATCH "linu*"',IDs) then
  9092. // ! // you have all matching IDs in IDs[]
  9093. TSQLRecordFTS3 = class(TSQLRecordVirtual)
  9094. public
  9095. /// optimize the FTS3 virtual table
  9096. // - this causes FTS3 to merge all existing index b-trees into a single large
  9097. // b-tree containing the entire index. This can be an expensive operation,
  9098. // but may speed up future queries. See http://sqlite.org/fts3.html#section_1_2
  9099. // - this method must be called server-side
  9100. // - returns TRUE on success
  9101. class function OptimizeFTS3Index(Server: TSQLRestServer): boolean;
  9102. /// this DocID property map the internal Row_ID property
  9103. // - but you can set a value to this property before calling the Add()
  9104. // method, to associate this TSQLRecordFTS3 to another TSQLRecord
  9105. // - ID property is read-only, but this DocID property can be written/set
  9106. // - internaly, we use RowID in the SQL statements, which is compatible
  9107. // with both TSQLRecord and TSQLRecordFTS3 kind of table
  9108. property DocID: TID read GetID write fID;
  9109. end;
  9110. /// this base class will create a FTS3 table using the Porter Stemming algorithm
  9111. // - see http://sqlite.org/fts3.html#tokenizer
  9112. TSQLRecordFTS3Porter = class(TSQLRecordFTS3);
  9113. /// this base class will create a FTS3 table using the Unicode61 Stemming algorithm
  9114. // - see http://sqlite.org/fts3.html#tokenizer
  9115. TSQLRecordFTS3Unicode61 = class(TSQLRecordFTS3);
  9116. /// class-reference type (metaclass) of a FTS3/FTS4 virtual table
  9117. TSQLRecordFTS3Class = class of TSQLRecordFTS3;
  9118. /// class-reference type (metaclass) of a RTREE virtual table
  9119. TSQLRecordRTreeClass = class of TSQLRecordRTree;
  9120. /// a base record, corresdonding to a FTS4 table, which is an enhancement to FTS3
  9121. // - FTS3 and FTS4 are nearly identical. They share most of their code in common,
  9122. // and their interfaces are the same. The only difference is that FTS4 stores
  9123. // some additional information about the document collection in two of new FTS
  9124. // shadow tables. This additional information allows FTS4 to use certain
  9125. // query performance optimizations that FTS3 cannot use. And the added information
  9126. // permits some additional useful output options in the matchinfo() function.
  9127. // - For newer applications, TSQLRecordFTS4 is recommended; though if minimal disk
  9128. // usage or compatibility with older versions of SQLite are important, then
  9129. // TSQLRecordFTS3 will usually serve just as well.
  9130. // - see http:// sqlite.org/fts3.html#section_1_1
  9131. TSQLRecordFTS4 = class(TSQLRecordFTS3)
  9132. public
  9133. /// this overriden method will create TRIGGERs for FTSWithoutContent()
  9134. class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
  9135. Options: TSQLInitializeTableOptions); override;
  9136. end;
  9137. /// this base class will create a FTS4 table using the Porter Stemming algorithm
  9138. // - see http://sqlite.org/fts3.html#tokenizer
  9139. TSQLRecordFTS4Porter = class(TSQLRecordFTS4);
  9140. /// this base class will create a FTS4 table using the Unicode61 Stemming algorithm
  9141. // - see http://sqlite.org/fts3.html#tokenizer
  9142. TSQLRecordFTS4Unicode61 = class(TSQLRecordFTS4);
  9143. /// the kind of fields to be available in a Table resulting of
  9144. // a TSQLRecordMany.DestGetJoinedTable() method call
  9145. // - Source fields are not available, because they will be always the same for
  9146. // a same SourceID, and they should be available from the TSQLRecord which
  9147. // hold the TSQLRecordMany instance
  9148. // - jkDestID and jkPivotID will retrieve only DestTable.ID and PivotTable.ID
  9149. // - jkDestFields will retrieve DestTable.* simple fields, or the fields
  9150. // specified by aCustomFieldsCSV (the Dest table name will be added: e.g.
  9151. // for aCustomFieldsCSV='One,Two', will retrieve DestTable.One, DestTable.Two)
  9152. // - jkPivotFields will retrieve PivotTable.* simple fields, or the fields
  9153. // specified by aCustomFieldsCSV (the Pivot table name will be added: e.g.
  9154. // for aCustomFieldsCSV='One,Two', will retrieve PivotTable.One, PivotTable.Two)
  9155. // - jkPivotAndDestAllFields for PivotTable.* and DestTable.* simple fields,
  9156. // or will retrieve the specified aCustomFieldsCSV fields (with
  9157. // the table name associated: e.g. 'PivotTable.One, DestTable.Two')
  9158. TSQLRecordManyJoinKind = (
  9159. jkDestID, jkPivotID, jkDestFields, jkPivotFields, jkPivotAndDestFields);
  9160. /// handle "has many" and "has many through" relationships
  9161. // - many-to-many relationship is tracked using a table specifically for that
  9162. // relationship, turning the relationship into two one-to-many relationships
  9163. // pointing in opposite directions
  9164. // - by default, only two TSQLRecord (i.e. INTEGER) fields must be created,
  9165. // named "Source" and "Dest", the first pointing to the source record (the one
  9166. // with a TSQLRecordMany published property) and the second to the destination record
  9167. // - you should first create a type inheriting from TSQLRecordMany, which
  9168. // will define the pivot table, providing optional "through" parameters if needed
  9169. // ! TSQLDest = class(TSQLRecord);
  9170. // ! TSQLSource = class;
  9171. // ! TSQLDestPivot = class(TSQLRecordMany)
  9172. // ! private
  9173. // ! fSource: TSQLSource;
  9174. // ! fDest: TSQLDest;
  9175. // ! fTime: TDateTime;
  9176. // ! published
  9177. // ! property Source: TSQLSource read fSource; // map Source column
  9178. // ! property Dest: TSQLDest read fDest; // map Dest column
  9179. // ! property AssociationTime: TDateTime read fTime write fTime;
  9180. // ! end;
  9181. // ! TSQLSource = class(TSQLRecord)
  9182. // ! private
  9183. // ! fDestList: TSQLDestPivot;
  9184. // ! published
  9185. // ! DestList: TSQLDestPivot read fDestList;
  9186. // ! end;
  9187. // - in all cases, at leat two 'Source' and 'Dest' published properties must
  9188. // be declared as TSQLRecord children in any TSQLRecordMany descendant
  9189. // because they will always be needed for the 'many to many' relationship
  9190. // - when a TSQLRecordMany published property exists in a TSQLRecord, it is
  9191. // initialized automaticaly by TSQLRecord.Create
  9192. // - to add some associations to the pivot table, use the ManyAdd() method
  9193. // - to retrieve an association, use the ManySelect() method
  9194. // - to delete an association, use the ManyDelete() method
  9195. // - to read all Dest records IDs, use the DestGet() method
  9196. // - to read the Dest records and the associated "through" fields content, use
  9197. // FillMany then FillRow, FillOne and FillRewind methods to loop through records
  9198. // - to read all Source records and the associaed "through" fields content,
  9199. // FillManyFromDest then FillRow, FillOne and FillRewind methods
  9200. // - to read all Dest IDs after a join to the pivot table, use DestGetJoined
  9201. TSQLRecordMany = class(TSQLRecord)
  9202. protected
  9203. // internal fields initialized during TSQLRecord.Create
  9204. // - map to the Source and Dest properties field values in TSQLRecord values
  9205. fSourceID: PPtrInt;
  9206. fDestID: PPtrInt;
  9207. /// retrieve the TSQLRecordMany ID from a given source+dest IDs pair
  9208. function InternalIDFromSourceDest(aClient: TSQLRest; aSourceID, aDestID: TID): TID;
  9209. function InternalFillMany(aClient: TSQLRest; aID: TID;
  9210. const aAndWhereSQL: RawUTF8; isDest: boolean): integer;
  9211. public
  9212. /// initialize this instance, and needed internal fields
  9213. // - will set protected fSourceID/fDestID fields
  9214. constructor Create; override;
  9215. /// retrieve all records associated to a particular source record, which
  9216. // has a TSQLRecordMany property
  9217. // - returns the Count of records corresponding to this aSource record
  9218. // - the records are stored in an internal TSQLTable, refered in the private
  9219. // fTable field, and initialized via a FillPrepare call: all Dest items
  9220. // are therefore accessible with standard FillRow, FillOne and FillRewind methods
  9221. // - use a "for .." loop or a "while FillOne do ..." loop to iterate
  9222. // through all Dest items, getting also any additional 'through' columns
  9223. // - if source ID parameter is 0, the ID is taken from the fSourceID field
  9224. // (set by TSQLRecord.Create)
  9225. // - note that if the Source record has just been added, fSourceID is not
  9226. // set, so this method will fail: please specify aSourceID parameter with
  9227. // the one just added/created
  9228. // - the optional aAndWhereSQL parameter can be used to add any additional
  9229. // condition to the WHERE statement (e.g. 'Salary>:(1000): AND Salary<:(2000):')
  9230. // according to TSQLRecordMany properties - note that you should better use
  9231. // inlined parameters for faster processing on server, so you may call e.g.
  9232. // ! aRec.FillMany(Client,0,FormatUTF8('Salary>? AND Salary<?',[],[1000,2000]));
  9233. function FillMany(aClient: TSQLRest; aSourceID: TID=0;
  9234. const aAndWhereSQL: RawUTF8=''): integer;
  9235. /// retrieve all records associated to a particular Dest record, which
  9236. // has a TSQLRecordMany property
  9237. // - returns the Count of records corresponding to this aSource record
  9238. // - use a "for .." loop or a "while FillOne do ..." loop to iterate
  9239. // through all Dest items, getting also any additional 'through' columns
  9240. // - the optional aAndWhereSQL parameter can be used to add any additional
  9241. // condition to the WHERE statement (e.g. 'Salary>:(1000): AND Salary<:(2000):')
  9242. // according to TSQLRecordMany properties - note that you should better use
  9243. // inlined parameters for faster processing on server, so you may call e.g.
  9244. // ! aRec.FillManyFromDest(Client,DestID,FormatUTF8('Salary>? AND Salary<?',[],[1000,2000]));
  9245. function FillManyFromDest(aClient: TSQLRest; aDestID: TID;
  9246. const aAndWhereSQL: RawUTF8=''): integer;
  9247. /// retrieve all Dest items IDs associated to the specified Source
  9248. function DestGet(aClient: TSQLRest; aSourceID: TID; out DestIDs: TIDDynArray): boolean; overload;
  9249. /// retrieve all Dest items IDs associated to the current Source ID
  9250. // - source ID is taken from the fSourceID field (set by TSQLRecord.Create)
  9251. // - note that if the Source record has just been added, fSourceID is not
  9252. // set, so this method will fail: please call the other overloaded method
  9253. function DestGet(aClient: TSQLRest; out DestIDs: TIDDynArray): boolean; overload;
  9254. /// retrieve all Source items IDs associated to the specified Dest ID
  9255. function SourceGet(aClient: TSQLRest; aDestID: TID; out SourceIDs: TIDDynArray): boolean;
  9256. /// retrieve all Dest items IDs associated to the current or
  9257. // specified Source ID, adding a WHERE condition against the Dest rows
  9258. // - if aSourceID is 0, the value is taken from current fSourceID field
  9259. // (set by TSQLRecord.Create)
  9260. // - aDestWhereSQL can specify the Dest table name in the statement, e.g.
  9261. // 'Salary>:(1000): AND Salary<:(2000):' - note that you should better use
  9262. // inlined parameters for faster processing on server, so you may use the
  9263. // more convenient function
  9264. // ! FormatUTF8('Salary>? AND Salary<?',[],[1000,2000])
  9265. // - this is faster than a manual FillMany() then loading each Dest,
  9266. // because the condition is executed in the SQL statement by the server
  9267. function DestGetJoined(aClient: TSQLRest; const aDestWhereSQL: RawUTF8;
  9268. aSourceID: TID; out DestIDs: TIDDynArray): boolean; overload;
  9269. /// create a Dest record, then FillPrepare() it to retrieve all Dest items
  9270. // associated to the current or specified Source ID, adding a WHERE condition
  9271. // against the Dest rows
  9272. // - if aSourceID is 0, the value is taken from current fSourceID field
  9273. // (set by TSQLRecord.Create)
  9274. // - aDestWhereSQL can specify the Dest table name in the statement, e.g.
  9275. // 'Salary>:(1000): AND Salary<:(2000):') according to TSQLRecordMany
  9276. // properties - note that you should better use such inlined parameters as
  9277. // ! FormatUTF8('Salary>? AND Salary<?',[],[1000,2000])
  9278. function DestGetJoined(aClient: TSQLRest; const aDestWhereSQL: RawUTF8;
  9279. aSourceID: TID): TSQLRecord; overload;
  9280. /// create a TSQLTable, containing all specified Fields, after a JOIN
  9281. // associated to the current or specified Source ID
  9282. // - the Table will have the fields specified by the JoinKind parameter
  9283. // - aCustomFieldsCSV can be used to specify which fields must be retrieved
  9284. // (for jkDestFields, jkPivotFields, jkPivotAndDestFields) - default is all
  9285. // - if aSourceID is 0, the value is taken from current fSourceID field
  9286. // (set by TSQLRecord.Create)
  9287. // - aDestWhereSQL can specify the Dest table name in the statement, e.g.
  9288. // 'Salary>:(1000): AND Salary<:(2000):') according to TSQLRecordMany
  9289. // properties - note that you should better use such inlined parameters as
  9290. // ! FormatUTF8('Salary>? AND Salary<?',[],[1000,2000])
  9291. function DestGetJoinedTable(aClient: TSQLRest; const aDestWhereSQL: RawUTF8;
  9292. aSourceID: TID; JoinKind: TSQLRecordManyJoinKind;
  9293. const aCustomFieldsCSV: RawUTF8=''): TSQLTable;
  9294. /// add a Dest record to the Source record list
  9295. // - returns TRUE on success, FALSE on error
  9296. // - if NoDuplicates is TRUE, the existence of this Source/Dest ID pair
  9297. // is first checked
  9298. // - current Source and Dest properties are filled with the corresponding
  9299. // TRecordReference values corresponding to the supplied IDs
  9300. // - any current value of the additional fields are used to populate the
  9301. // newly created content (i.e. all published properties of this record)
  9302. // - if aUseBatch is set, it will use this TSQLRestBach.Add() instead
  9303. // of the slower aClient.Add() method
  9304. function ManyAdd(aClient: TSQLRest; aSourceID, aDestID: TID;
  9305. NoDuplicates: boolean=false; aUseBatch: TSQLRestBatch=nil): boolean; overload;
  9306. /// add a Dest record to the current Source record list
  9307. // - source ID is taken from the fSourceID field (set by TSQLRecord.Create)
  9308. // - note that if the Source record has just been added, fSourceID is not
  9309. // set, so this method will fail: please call the other overloaded method
  9310. function ManyAdd(aClient: TSQLRest; aDestID: TID;
  9311. NoDuplicates: boolean=false): boolean; overload;
  9312. /// will delete the record associated with a particular Source/Dest pair
  9313. // - will return TRUE if the pair was found and successfully deleted
  9314. // - if aUseBatch is set, it will use this TSQLRestBach.Delete() instead
  9315. // of the slower aClient.Delete() method
  9316. function ManyDelete(aClient: TSQLRest; aSourceID, aDestID: TID;
  9317. aUseBatch: TSQLRestBatch=nil): boolean; overload;
  9318. /// will delete the record associated with the current source and a specified Dest
  9319. // - source ID is taken from the fSourceID field (set by TSQLRecord.Create)
  9320. // - note that if the Source record has just been added, fSourceID is not
  9321. // set, so this method will fail: please call the other overloaded method
  9322. function ManyDelete(aClient: TSQLRest; aDestID: TID): boolean; overload;
  9323. /// will retrieve the record associated with a particular Source/Dest pair
  9324. // - will return TRUE if the pair was found
  9325. // - in this case, all "through" columns are available in the TSQLRecordMany
  9326. // field instance
  9327. function ManySelect(aClient: TSQLRest; aSourceID, aDestID: TID): boolean; overload;
  9328. /// will retrieve the record associated with the current source and a specified Dest
  9329. // - source ID is taken from the fSourceID field (set by TSQLRecord.Create)
  9330. // - note that if the Source record has just been added, fSourceID is not
  9331. // set, so this method will fail: please call the other overloaded method
  9332. function ManySelect(aClient: TSQLRest; aDestID: TID): boolean; overload;
  9333. // get the SQL WHERE statement to be used to retrieve the associated
  9334. // records according to a specified ID
  9335. // - search for aID as Source ID if isDest is FALSE
  9336. // - search for aID as Dest ID if isDest is TRUE
  9337. // - the optional aAndWhereSQL parameter can be used to add any additional
  9338. // condition to the WHERE statement (e.g. 'Salary>:(1000): AND Salary<:(2000):')
  9339. // according to TSQLRecordMany properties - note that you should better use
  9340. // such inlined parameters e.g. calling
  9341. // ! FormatUTF8('Salary>? AND Salary<?',[],[1000,2000])
  9342. function IDWhereSQL(aClient: TSQLRest; aID: TID; isDest: boolean;
  9343. const aAndWhereSQL: RawUTF8=''): RawUTF8;
  9344. end;
  9345. /// a base record, with a JSON-logging capability
  9346. // - used to store a log of events into a JSON text, easy to be displayed
  9347. // with a TSQLTableToGrid
  9348. // - this log can then be stored as a RawUTF8 field property into a result
  9349. // record, for instance
  9350. TSQLRecordLog = class(TSQLRecord)
  9351. protected
  9352. /// store the Log Table JSON content
  9353. fLogTableStorage: TMemoryStream;
  9354. /// used by Log() to add the value of OneLog to fLogTableStorage
  9355. fLogTableWriter: TJSONSerializer;
  9356. /// current internal row count
  9357. fLogTableRowCount: integer;
  9358. /// maximum rows count
  9359. fMaxLogTableRowCount: integer;
  9360. public
  9361. /// initialize the internal storage with a supplied JSON content
  9362. // - this JSON content must follow the format retrieved by
  9363. // LogTableJSON and LogTableJSONFrom methods
  9364. constructor CreateFrom(OneLog: TSQLRecord; const aJSON: RawUTF8);
  9365. /// release the private fLogTableWriter and fLogTableStorage objects
  9366. destructor Destroy; override;
  9367. /// add the value of OneLog to the Log Table JSON content
  9368. // - the ID property of the supplied OneLog record is incremented before adding
  9369. procedure Log(OneLog: TSQLRecord);
  9370. /// returns the JSON data as added by previous call to Log()
  9371. // - JSON data is in not-expanded format
  9372. // - this function can be called multiple times
  9373. function LogTableJSON: RawUTF8;
  9374. /// returns the internal position of the Log content
  9375. // - use this value to later retrieve a log range with LogTableJSONFrom()
  9376. function LogCurrentPosition: integer;
  9377. /// returns the log JSON data from a given start position
  9378. // - StartPosition was retrieved previously with LogCurrentPosition
  9379. // - if StartPosition=0, the whole Log content is returned
  9380. // - multiple instances of LogCurrentPosition/LogTableJSONFrom() can be
  9381. // used at once
  9382. function LogTableJSONFrom(StartPosition: integer): RawUTF8;
  9383. /// the current associated Log Table rows count value
  9384. // - is incremented every time Log() method is called
  9385. // - will be never higher than MaxLogTableRowCount below (if set)
  9386. property LogTableRowCount: integer read fLogTableRowCount;
  9387. /// if the associated Log Table rows count reachs this value, the
  9388. // first data row will be trimed
  9389. // - do nothing is value is left to 0 (which is the default)
  9390. // - total rows count won't never be higher than this value
  9391. // - used to spare memory usage
  9392. property MaxLogTableRowCount: integer read fMaxLogTableRowCount;
  9393. end;
  9394. /// common ancestor for tables with digitally signed RawUTF8 content
  9395. // - content is signed according to a specific User Name and the digital
  9396. // signature date and time
  9397. // - internaly uses the very secure SHA-256 hashing algorithm for performing
  9398. // the digital signature
  9399. TSQLRecordSigned = class(TSQLRecord)
  9400. protected
  9401. /// time and date of signature
  9402. fSignatureTime: TTimeLog;
  9403. /// hashed signature
  9404. fSignature: RawUTF8;
  9405. public
  9406. /// time and date of signature
  9407. // - if the signature is invalid, this field will contain numerical 1 value
  9408. // - this property is defined here to allow inherited to just declared the name
  9409. // in its published section:
  9410. // ! property SignatureTime;
  9411. property SignatureTime: TTimeLog read fSignatureTime write fSignatureTime;
  9412. /// as the Content of this record is added to the database,
  9413. // its value is hashed and stored as 'UserName/03A35C92....' into this property
  9414. // - very secured SHA-256 hashing is used internaly
  9415. // - digital signature is allowed only once: this property is written only once
  9416. // - this property is defined here to allow inherited to just declared the name
  9417. // in its published section:
  9418. // ! property SignatureTime;
  9419. property Signature: RawUTF8 read fSignature write fSignature;
  9420. public
  9421. /// use this procedure to sign the supplied Content of this record for a
  9422. // specified UserName, with the current Date and Time (SHA-256 hashing is used
  9423. // internaly)
  9424. // - returns true if signed successfully (not already signed)
  9425. function SetAndSignContent(const UserName: RawUTF8;
  9426. const Content: RawByteString; ForcedSignatureTime: Int64=0): boolean;
  9427. /// returns true if this record content is correct according to the
  9428. // stored digital Signature
  9429. function CheckSignature(const Content: RawByteString): boolean;
  9430. /// retrieve the UserName who digitally signed this record
  9431. // - returns '' if was not digitally signed
  9432. function SignedBy: RawUTF8;
  9433. /// reset the stored digital signature
  9434. // - SetAndSignContent() can be called after this method
  9435. procedure UnSign;
  9436. end;
  9437. /// a base record, which would have creation and modification timestamp fields
  9438. TSQLRecordTimed = class(TSQLRecord)
  9439. protected
  9440. fCreated: TCreateTime;
  9441. fModified: TModTime;
  9442. published
  9443. /// will be filled by the ORM when this item will be created in the database
  9444. property Created: TCreateTime read fCreated write fCreated;
  9445. /// will be filled by the ORM each time this item will be written in the database
  9446. property Modified: TModTime read fModified write fModified;
  9447. end;
  9448. /// common ancestor for tables which should implement any interface
  9449. // - by default, TSQLRecord does not implement any interface: this does make
  9450. // sense for performance and resource use reasons
  9451. // - inherit from this class if you want your class to implement the needed
  9452. // IInterface methods (QueryInterface/AddRef/Release)
  9453. TSQLRecordInterfaced = class(TSQLRecord, IInterface)
  9454. protected
  9455. fRefCount: Integer;
  9456. {$ifdef FPC}
  9457. function QueryInterface(
  9458. {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID;
  9459. out Obj): longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  9460. function _AddRef: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  9461. function _Release: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  9462. {$else}
  9463. function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
  9464. function _AddRef: Integer; stdcall;
  9465. function _Release: Integer; stdcall;
  9466. {$endif}
  9467. public
  9468. class function NewInstance: TObject; override;
  9469. procedure AfterConstruction; override;
  9470. procedure BeforeDestruction; override;
  9471. property RefCount: Integer read fRefCount;
  9472. end;
  9473. /// the possible Server-side instance implementation patterns for
  9474. // interface-based services
  9475. // - each interface-based service will be implemented by a corresponding
  9476. // class instance on the server: this parameter is used to define how
  9477. // class instances are created and managed
  9478. // - on the Client-side, each instance will be handled depending on the
  9479. // server side implementation (i.e. with sicClientDriven behavior if necessary)
  9480. // - sicSingle: one object instance is created per call - this is the
  9481. // most expensive way of implementing the service, but is safe for simple
  9482. // workflows (like a one-type call); this is the default setting for
  9483. // TSQLRestServer.ServiceRegister method
  9484. // - sicShared: one object instance is used for all incoming calls and is
  9485. // not recycled subsequent to the calls - the implementation should be
  9486. // thread-safe on the server side
  9487. // - sicClientDriven: one object instance will be created in synchronization
  9488. // with the client-side lifetime of the corresponding interface: when the
  9489. // interface will be released on client, it will be released on the server
  9490. // side - a numerical identifier will be transmitted for all JSON requests
  9491. // - sicPerSession, sicPerUser and sicPerGroup modes will maintain one
  9492. // object instance per running session / user / group (only working if
  9493. // RESTful authentication is enabled) - since it may be shared among users or
  9494. // groups, the sicPerUser and sicPerGroup implementation should be thread-safe
  9495. // - sicPerThread will maintain one object instance per calling thread - it
  9496. // may be useful instead of sicShared mode if the service process expects
  9497. // some per-heavy thread initialization, for instance
  9498. TServiceInstanceImplementation = (
  9499. sicSingle, sicShared, sicClientDriven, sicPerSession, sicPerUser, sicPerGroup,
  9500. sicPerThread);
  9501. /// set of Server-side instance implementation patterns for
  9502. // interface-based services
  9503. TServiceInstanceImplementations = set of TServiceInstanceImplementation;
  9504. /// handled kind of parameters for an interface-based service provider method
  9505. // - we do not handle all kind of Delphi variables, but provide some
  9506. // enhanced types handled by JSONToObject/ObjectToJSON functions (smvObject)
  9507. // or TDynArray.LoadFromJSON / TTextWriter.AddDynArrayJSON methods (smvDynArray)
  9508. // - records will be serialized as Base64 string, with our RecordSave/RecordLoad
  9509. // low-level format by default, or as true JSON objects since Delphi 2010 or
  9510. // after registration via a TTextWriter.RegisterCustomJSONSerializer call
  9511. // - smvRawJSON will transmit the raw JSON content, without serialization
  9512. TServiceMethodValueType = (
  9513. smvNone,
  9514. smvSelf,
  9515. smvBoolean,
  9516. smvEnum,
  9517. smvSet,
  9518. smvInteger,
  9519. smvCardinal,
  9520. smvInt64,
  9521. smvDouble,
  9522. smvDateTime,
  9523. smvCurrency,
  9524. smvRawUTF8,
  9525. smvString,
  9526. smvRawByteString,
  9527. smvWideString,
  9528. smvRecord,
  9529. {$ifndef NOVARIANTS}
  9530. smvVariant,
  9531. {$endif}
  9532. smvObject,
  9533. smvRawJSON,
  9534. smvDynArray,
  9535. smvInterface);
  9536. /// handled kind of parameters internal variables for an interface-based method
  9537. // - reference-counted variables will have their own storage
  9538. // - all non referenced-counted variables are stored within some 64-bit content
  9539. // - smvVariant kind of parameter will be handled as a special smvvRecord
  9540. TServiceMethodValueVar = (
  9541. smvvNone, smvvSelf, smvv64, smvvRawUTF8, smvvString, smvvWideString,
  9542. smvvRecord, smvvObject, smvvDynArray, smvvInterface);
  9543. /// set of parameters for an interface-based service provider method
  9544. TServiceMethodValueTypes = set of TServiceMethodValueType;
  9545. /// handled kind of parameters direction for an interface-based service method
  9546. // - IN, IN/OUT, OUT directions can be applied to arguments, and will
  9547. // be available through our JSON-serialized remote access: smdVar and smdOut
  9548. // kind of parameters will be returned within the "result": JSON array
  9549. // - smdResult is used for a function method, to handle the returned value
  9550. TServiceMethodValueDirection = (
  9551. smdConst,
  9552. smdVar,
  9553. smdOut,
  9554. smdResult);
  9555. /// set of parameters direction for an interface-based service method
  9556. TServiceMethodValueDirections = set of TServiceMethodValueDirection;
  9557. /// describe a service provider method argument
  9558. TServiceMethodArgument = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  9559. public
  9560. /// the argument name, as declared in Delphi
  9561. ParamName: PShortString;
  9562. /// the type name, as declared in Delphi
  9563. ArgTypeName: PShortString;
  9564. /// the low-level RTTI information of this argument
  9565. ArgTypeInfo: PTypeInfo;
  9566. /// we do not handle all kind of Delphi variables
  9567. ValueType: TServiceMethodValueType;
  9568. /// the variable direction as defined at code level
  9569. ValueDirection: TServiceMethodValueDirection;
  9570. /// how the variable may be stored
  9571. ValueVar: TServiceMethodValueVar;
  9572. /// how the variable is to be passed at asm level
  9573. // - vIsString is included for smvRawUTF8, smvString, smvRawByteString and
  9574. // smvWideString kind of parameter (smvRecord has it to false, even if they
  9575. // are Base-64 encoded within the JSON content, and also smvVariant/smvRawJSON)
  9576. // - vPassedByReference is included if the parameter is passed as reference
  9577. // (i.e. defined as var/out, or is a record or a reference-counted type result)
  9578. // - vIsObjArray is set if the dynamic array is a T*ObjArray, so should be
  9579. // cleared with ObjArrClear() and not TDynArray.Clear
  9580. ValueKindAsm: set of (vIsString, vPassedByReference, vIsObjArray);
  9581. /// byte offset in the CPU stack of this argument
  9582. // - may be -1 if pure register parameter with no backup on stack (x86)
  9583. InStackOffset: integer;
  9584. /// used to specify if the argument is passed as register
  9585. // - contains 0 if parameter is not a register
  9586. // - contains 1 for EAX, 2 for EDX and 3 for ECX registers for x86
  9587. // - contains 1 for RCX, 2 for RDX, 3 for R8, and
  9588. // 4 for R9, with a backing store on the stack for x64
  9589. // - contains 1 for R0, 2 R1 ... 4 for R3, with a backing store on the stack for arm
  9590. // - contains 1 for X0, 2 X1 ... 8 for X7, with a backing store on the stack for aarch64
  9591. RegisterIdent: integer;
  9592. /// used to specify if a floating-point argument is passed as register
  9593. // - contains always 0 for x86/x87
  9594. // - contains 1 for XMM0, 2 for XMM1 ... 4 for XMM3 for x64
  9595. // - contains 1 for D0, 2 D1 ... 8 for D7 for armhf
  9596. // - contains 1 for V0, 2 V1 ... 8 for V7 for aarch64
  9597. FPRegisterIdent: integer;
  9598. /// size (in bytes) of this argument on the stack
  9599. SizeInStack: integer;
  9600. /// size (in bytes) of this smvv64 ordinal value
  9601. // - e.g. depending of the associated kind of enumeration
  9602. SizeInStorage: integer;
  9603. /// index of the associated variable in the local array[ArgsUsedCount[]]
  9604. // - for smdConst argument, contains -1 (no need to a local var: the value
  9605. // will be on the stack only)
  9606. IndexVar: integer;
  9607. {$ifndef FPC}
  9608. /// set ArgTypeName and ArgTypeInfo values from RTTI
  9609. procedure SetFromRTTI(var P: PByte);
  9610. {$endif}
  9611. /// serialize the argument into the TServiceContainer.Contract JSON format
  9612. // - non standard types (e.g. clas, enumerate, dynamic array or record)
  9613. // are identified by their type identifier - so contract does not extend
  9614. // up to the content of such high-level structures
  9615. procedure SerializeToContract(WR: TTextWriter);
  9616. /// append the JSON value corresponding to this argument
  9617. // - includes a pending ','
  9618. procedure AddJSON(WR: TTextWriter; V: pointer);
  9619. /// append the value corresponding to this argument as within a JSON string
  9620. // - will escape any JSON string character, and include a pending ','
  9621. procedure AddJSONEscaped(WR: TTextWriter; V: pointer);
  9622. /// append the JSON value corresponding to this argument, from its text value
  9623. // - includes a pending ','
  9624. procedure AddValueJSON(WR: TTextWriter; const Value: RawUTF8);
  9625. /// append the default JSON value corresponding to this argument
  9626. // - includes a pending ','
  9627. procedure AddDefaultJSON(WR: TTextWriter);
  9628. /// convert a value into its JSON representation
  9629. procedure AsJson(var DestValue: RawUTF8; V: pointer);
  9630. {$ifndef NOVARIANTS}
  9631. /// convert a value into its variant representation
  9632. // - complex objects would be converted into a TDocVariant, after JSON
  9633. // serialization: variant conversion options may e.g. be retrieve from
  9634. // TInterfaceFactory.DocVariantOptions
  9635. procedure AsVariant(var DestValue: variant; V: pointer;
  9636. Options: TDocVariantOptions);
  9637. /// add a value into a TDocVariant object or array
  9638. // - Dest should already have set its Kind to either dvObject or dvArray
  9639. procedure AddAsVariant(var Dest: TDocVariantData; V: pointer);
  9640. /// normalize a value containing one input or output argument
  9641. // - sets and enumerates would be translated to strings (also in embedded
  9642. // objects and T*ObjArray)
  9643. procedure FixValue(var Value: variant);
  9644. /// normalize a value containing one input or output argument, and add
  9645. // it to a destination variant Document
  9646. // - sets and enumerates would be translated to strings (also in embedded
  9647. // objects and T*ObjArray)
  9648. procedure FixValueAndAddToObject(const Value: variant; var DestDoc: TDocVariantData);
  9649. {$endif}
  9650. end;
  9651. /// describe a service provider method arguments
  9652. TServiceMethodArgumentDynArray = array of TServiceMethodArgument;
  9653. /// callback called by TServiceMethodExecute to process an interface
  9654. // callback parameter
  9655. // - implementation should set the Obj local variable to an instance of
  9656. // a fake class implementing the aParamInfo interface
  9657. TServiceMethodExecuteCallback =
  9658. procedure(var Par: PUTF8Char; ParamInterfaceInfo: PTypeInfo; out Obj) of object;
  9659. /// how TServiceMethod.TServiceMethod method would return the generated document
  9660. // - will return either a dvObject or dvArray TDocVariantData, depending on
  9661. // the expected returned document layout
  9662. // - returned content could be "normalized" (for any set or enumerate) if
  9663. // Kind is pdvObjectFixed
  9664. TServiceMethodParamsDocVariantKind = (pdvArray, pdvObject, pdvObjectFixed);
  9665. /// describe an interface-based service provider method
  9666. TServiceMethod = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  9667. public
  9668. /// the method URI
  9669. // - basicaly the method name as declared in Delphi code (e.g. 'Add' for
  9670. // ICalculator.Add)
  9671. // - this property value is hashed internaly for faster access
  9672. URI: RawUTF8;
  9673. /// the method default result, formatted as a JSON array
  9674. // - example of content may be '[]' for a procedure or '[0]' for a function
  9675. // - any var/out and potential function result will be set as a JSON array
  9676. // of values, with 0 for numerical values, "" for textual values,
  9677. // false for booleans, [] for dynamic arrays, a void record serialized
  9678. // as expected (including customized serialization) and null for objects
  9679. DefaultResult: RawUTF8;
  9680. /// the fully qualified dotted method name, including the interface name
  9681. // - as used by TServiceContainerInterfaceMethod.InterfaceDotMethodName
  9682. // - match the URI fullpath name, e.g. 'Calculator.Add'
  9683. InterfaceDotMethodName: RawUTF8;
  9684. /// method index in the original (non emulated) interface
  9685. // - our custom methods start at index 3 (RESERVED_VTABLE_SLOTS), since
  9686. // QueryInterface, _AddRef, and _Release are always defined by default
  9687. // - so it maps TServiceFactory.Interface.Methods[ExecutionMethodIndex-3]
  9688. ExecutionMethodIndex: byte;
  9689. /// TRUE if the method is inherited from another parent interface
  9690. IsInherited: boolean;
  9691. /// is 0 for the root interface, 1..n for all inherited interfaces
  9692. HierarchyLevel: byte;
  9693. /// describe expected method arguments
  9694. // - Args[0] always is smvSelf
  9695. // - if method is a function, an additional smdResult argument is appended
  9696. Args: TServiceMethodArgumentDynArray;
  9697. /// the index of the result pseudo-argument in Args[]
  9698. // - is -1 if the method is defined as a (not a function)
  9699. ArgsResultIndex: shortint;
  9700. /// the index of the first const / var argument in Args[]
  9701. ArgsInFirst: shortint;
  9702. /// the index of the last const / var argument in Args[]
  9703. ArgsInLast: shortint;
  9704. /// the index of the first var / out / result argument in Args[]
  9705. ArgsOutFirst: shortint;
  9706. /// the index of the last var / out / result argument in Args[]
  9707. ArgsOutLast: shortint;
  9708. /// the index of the last argument in Args[], excepting result
  9709. ArgsNotResultLast: shortint;
  9710. /// the index of the last var / out argument in Args[]
  9711. ArgsOutNotResultLast: shortint;
  9712. /// the number of const / var parameters in Args[]
  9713. // - i.e. the number of elements in the input JSON array
  9714. ArgsInputValuesCount: byte;
  9715. /// the number of var / out parameters + in Args[]
  9716. // - i.e. the number of elements in the output JSON array or object
  9717. ArgsOutputValuesCount: byte;
  9718. /// true if the result is a TServiceCustomAnswer record
  9719. // - that is, a custom Header+Content BLOB transfert, not a JSON object
  9720. ArgsResultIsServiceCustomAnswer: boolean;
  9721. /// the index of the first argument expecting manual stack initialization
  9722. // - set if there is any smvObject,smvDynArray,smvRecord,smvInterface or
  9723. // smvVariant
  9724. ArgsManagedFirst: shortint;
  9725. /// the index of the last argument expecting manual stack initialization
  9726. // - set if there is any smvObject,smvDynArray,smvRecord, smvInterface or
  9727. // smvVariant
  9728. ArgsManagedLast: shortint;
  9729. /// contains all used kind of arguments
  9730. ArgsUsed: TServiceMethodValueTypes;
  9731. /// contains the count of variables for all used kind of arguments
  9732. ArgsUsedCount: array[TServiceMethodValueVar] of byte;
  9733. /// needed CPU stack size (in bytes) for all arguments
  9734. // - under x64, does not include the backup space for the four registers
  9735. ArgsSizeInStack: cardinal;
  9736. /// retrieve an argument index in Args[] from its name
  9737. // - search is case insensitive
  9738. // - if Input is TRUE, will search within const / var arguments
  9739. // - if Input is FALSE, will search within var / out / result arguments
  9740. // - returns -1 if not found
  9741. function ArgIndex(ArgName: PUTF8Char; ArgNameLen: integer; Input: boolean): integer;
  9742. /// find the next argument index in Args[]
  9743. // - if Input is TRUE, will search within const / var arguments
  9744. // - if Input is FALSE, will search within var / out / result arguments
  9745. // - returns true if arg is the new value, false otherwise
  9746. function ArgNext(var arg: integer; Input: boolean): boolean;
  9747. /// convert parameters encoded as a JSON array into a JSON object
  9748. // - if Input is TRUE, will handle const / var arguments
  9749. // - if Input is FALSE, will handle var / out / result arguments
  9750. function ArgsArrayToObject(P: PUTF8Char; Input: boolean): RawUTF8;
  9751. /// returns a dynamic array list of all parameter names
  9752. // - if Input is TRUE, will handle const / var arguments
  9753. // - if Input is FALSE, will handle var / out / result arguments
  9754. function ArgsNames(Input: Boolean): TRawUTF8DynArray;
  9755. {$ifndef NOVARIANTS}
  9756. /// computes a TDocVariant containing the input or output arguments values
  9757. // - Values[] should contain the input/output raw values as variant
  9758. // - Kind would specify the expected returned document layout
  9759. procedure ArgsValuesAsDocVariant(Kind: TServiceMethodParamsDocVariantKind;
  9760. out Dest: TDocVariantData; const Values: TVariantDynArray; Input: boolean;
  9761. Options: TDocVariantOptions=[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]);
  9762. /// normalize a TDocVariant containing the input or output arguments values
  9763. // - "normalization" will ensure sets and enums are seralized as text
  9764. // - if Input is TRUE, will handle const / var arguments
  9765. // - if Input is FALSE, will handle var / out / result arguments
  9766. procedure ArgsAsDocVariantFix(var ArgsObject: TDocVariantData; Input: boolean);
  9767. /// convert a TDocVariant array containing the input or output arguments
  9768. // values in order, into an object with named parameters
  9769. // - here sets and enums would keep their current values, mainly numerical
  9770. // - if Input is TRUE, will handle const / var arguments
  9771. // - if Input is FALSE, will handle var / out / result arguments
  9772. procedure ArgsAsDocVariantObject(const ArgsParams: TDocVariantData;
  9773. var ArgsObject: TDocVariantData; Input: boolean);
  9774. /// computes a TDocVariant containing the input or output arguments values
  9775. // - Values[] should point to the input/output raw binary values, as stored
  9776. // in TServiceMethodExecute.Values during execution
  9777. procedure ArgsStackAsDocVariant(const Values: TPPointerDynArray;
  9778. out Dest: TDocVariantData; Input: Boolean);
  9779. {$endif}
  9780. end;
  9781. /// describe all mtehods of an interface-based service provider
  9782. TServiceMethodDynArray = array of TServiceMethod;
  9783. /// a pointer to an interface-based service provider method description
  9784. // - since TInterfaceFactory instances are shared in a global list, we
  9785. // can safely use such pointers in our code to refer to a particular method
  9786. PServiceMethod = ^TServiceMethod;
  9787. /// common ancestor for storing interface-based service execution statistics
  9788. // - each call could be logged and monitored in the database
  9789. // - TServiceMethodExecute could store all its calls in such a table
  9790. // - enabled on server side via either TServiceFactoryServer.SetServiceLog or
  9791. // TServiceContainerServer.SetServiceLog method
  9792. TSQLRecordServiceLog = class(TSQLRecord)
  9793. protected
  9794. fMethod: RawUTF8;
  9795. fInput: variant;
  9796. fOutput: variant;
  9797. fUser: integer;
  9798. fSession: integer;
  9799. fTime: TModTime;
  9800. fMicroSec: integer;
  9801. // define Input/Output as dvoSerializeAsExtendedJson
  9802. class procedure InternalDefineModel(Props: TSQLRecordProperties); override;
  9803. public
  9804. /// overriden method creating an index on the Method/MicroSec columns
  9805. class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
  9806. Options: TSQLInitializeTableOptions); override;
  9807. published
  9808. /// the 'interface.method' identifier of this call
  9809. // - this column will be indexed, for fast SQL queries, with the MicroSec
  9810. // column (for performance tuning)
  9811. property Method: RawUTF8 read fMethod write fMethod;
  9812. /// the input parameters, as a JSON document
  9813. // - will be stored in JSON_OPTIONS_FAST_EXTENDED format, i.e. with
  9814. // shortened field names, for smaller TEXT storage
  9815. // - content may be searched using JsonGet/JsonHas SQL functions on a
  9816. // SQlite3 storage, or with direct document query under MongoDB/PostgreSQL
  9817. property Input: variant read fInput write fInput;
  9818. /// the output parameters, as a JSON document, including result: for a function
  9819. // - will be stored in JSON_OPTIONS_FAST_EXTENDED format, i.e. with
  9820. // shortened field names, for smaller TEXT storage
  9821. // - content may be searched using JsonGet/JsonHas SQL functions on a
  9822. // SQlite3 storage, or with direct document query under MongoDB/PostgreSQL
  9823. property Output: variant read fOutput write fOutput;
  9824. /// the Session ID, if there is any
  9825. property Session: integer read fSession write fSession;
  9826. /// the User ID, if there is an identified Session
  9827. property User: integer read fUser write fUser;
  9828. /// will be filled by the ORM when this record is written in the database
  9829. property Time: TModTime read fTime write fTime;
  9830. /// execution time of this method, in micro seconds
  9831. property MicroSec: integer read fMicroSec write fMicroSec;
  9832. end;
  9833. /// execution statistics used for DB-based asynchronous notifications
  9834. // - as used by TServiceFactoryClient.SendNotifications
  9835. // - here, the Output column may contain the information about an error
  9836. // occurred during process
  9837. TSQLRecordServiceNotifications = class(TSQLRecordServiceLog)
  9838. protected
  9839. fSent: TTimeLog;
  9840. public
  9841. /// this overriden method will create an index on the 'Sent' column
  9842. class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
  9843. Options: TSQLInitializeTableOptions); override;
  9844. /// search for pending events since a supplied ID
  9845. // - returns FALSE if no notification was found
  9846. // - returns TRUE ad fill a TDocVariant array of JSON Objects, including
  9847. // "ID": field, and Method as "MethodName": field
  9848. class function LastEventsAsObjects(Rest: TSQLRest; LastKnownID: TID; Limit: integer;
  9849. Service: TInterfaceFactory; out Dest: TDocVariantData;
  9850. const MethodName: RawUTF8 = 'Method'; IDAsHexa: boolean = false): boolean;
  9851. /// allows to convert the Input array into a proper single JSON Object
  9852. // - "ID": field would be included, and Method as "MethodName": field
  9853. function SaveInputAsObject(Service: TInterfaceFactory;
  9854. const MethodName: RawUTF8 = 'Method'; IDAsHexa: boolean = false): variant; virtual;
  9855. /// run FillOne and SaveInputAsObject into a TDocVariant array of JSON Objects
  9856. // - "ID": field would be included, and Method as "MethodName": field
  9857. procedure SaveFillInputsAsObjects(Service: TInterfaceFactory; out Dest: TDocVariantData;
  9858. const MethodName: RawUTF8 = 'Method'; IDAsHexa: boolean = false);
  9859. published
  9860. /// when this notification has been sent
  9861. // - equals 0 until it was actually notified
  9862. property Sent: TTimeLog read fSent write fSent;
  9863. end;
  9864. /// class-reference type (metaclass) for storing interface-based service
  9865. // execution statistics
  9866. // - you could inherit from TSQLRecordServiceLog, and specify additional
  9867. // fields corresponding to the execution context
  9868. TSQLRecordServiceLogClass = class of TSQLRecordServiceLog;
  9869. /// class-reference type (metaclass) for storing interface-based service
  9870. // execution statistics used for DB-based asynchronous notifications
  9871. // - as used by TServiceFactoryClient.SendNotifications
  9872. TSQLRecordServiceNotificationsClass = class of TSQLRecordServiceNotifications;
  9873. TServiceMethodExecute = class;
  9874. /// the current step of a TServiceMethodExecute.OnExecute call
  9875. TServiceMethodExecuteEventStep = (smsUndefined, smsBefore, smsAfter, smsError);
  9876. /// the TServiceMethodExecute.OnExecute signature
  9877. TServiceMethodExecuteEvent = procedure(Sender: TServiceMethodExecute;
  9878. Step: TServiceMethodExecuteEventStep) of object;
  9879. /// execute a method of a TInterfacedObject instance, from/to JSON
  9880. TServiceMethodExecute = class
  9881. protected
  9882. fMethod: PServiceMethod;
  9883. fRawUTF8s: TRawUTF8DynArray;
  9884. fStrings: TStringDynArray;
  9885. fWideStrings: TWideStringDynArray;
  9886. fRecords: array of TBytes;
  9887. fInt64s: TInt64DynArray;
  9888. fObjects: TObjectDynArray;
  9889. fInterfaces: TPointerDynArray;
  9890. fDynArrays: array of record
  9891. Value: Pointer;
  9892. Wrapper: TDynArray;
  9893. end;
  9894. fValues: TPPointerDynArray;
  9895. fAlreadyExecuted: boolean;
  9896. fTempTextWriter: TJSONSerializer;
  9897. fOnExecute: array of TServiceMethodExecuteEvent;
  9898. fBackgroundExecutionThread: TSynBackgroundThreadMethod;
  9899. fOnCallback: TServiceMethodExecuteCallback;
  9900. fOptions: TServiceMethodOptions;
  9901. fServiceCustomAnswerHead: RawUTF8;
  9902. fServiceCustomAnswerStatus: cardinal;
  9903. fLastException: Exception;
  9904. fInput: TDocVariantData;
  9905. fOutput: TDocVariantData;
  9906. fCurrentStep: TServiceMethodExecuteEventStep;
  9907. procedure BeforeExecute;
  9908. procedure RawExecute(const Instances: PPointerArray; InstancesLast: integer);
  9909. procedure AfterExecute;
  9910. public
  9911. /// initialize the execution instance
  9912. constructor Create(aMethod: PServiceMethod);
  9913. /// finalize the execution instance
  9914. destructor Destroy; override;
  9915. /// allow to hook method execution
  9916. // - if optInterceptInputOutput is defined in Options, then Sender.Input/Output
  9917. // fields would contain the execution data context when Hook is called
  9918. procedure AddInterceptor(const Hook: TServiceMethodExecuteEvent);
  9919. /// execute the corresponding method of a given TInterfacedObject instance
  9920. // - will retrieve a JSON array of parameters from Par
  9921. // - will append a JSON array of results in Res, or set an Error message, or
  9922. // a JSON object (with parameter names) in Res if ResultAsJSONObject is set
  9923. function ExecuteJson(const Instances: array of pointer; Par: PUTF8Char;
  9924. Res: TTextWriter; ResAsJSONObject: boolean=false): boolean;
  9925. /// low-level direct access to the associated method information
  9926. property Method: PServiceMethod read fMethod;
  9927. /// low-level direct access to the current input/output parameter values
  9928. // - you should not need to access this, but rather set
  9929. // optInterceptInputOutput in Options, and read Input/Output content
  9930. property Values: TPPointerDynArray read fValues;
  9931. /// associated settings, as copied from TServiceFactoryServer.Options
  9932. property Options: TServiceMethodOptions read fOptions write fOptions;
  9933. /// the current state of the execution
  9934. property CurrentStep: TServiceMethodExecuteEventStep
  9935. read fCurrentStep write fCurrentStep;
  9936. /// set from output TServiceCustomAnswer.Header result parameter
  9937. property ServiceCustomAnswerHead: RawUTF8
  9938. read fServiceCustomAnswerHead write fServiceCustomAnswerHead;
  9939. /// set from output TServiceCustomAnswer.Status result parameter
  9940. property ServiceCustomAnswerStatus: cardinal
  9941. read fServiceCustomAnswerStatus write fServiceCustomAnswerStatus;
  9942. /// set if optInterceptInputOutput is defined in TServiceFactoryServer.Options
  9943. // - contains a dvObject with input parameters as "argname":value pairs
  9944. // - this is a read-only property: you cannot change the input content
  9945. property Input: TDocVariantData read fInput;
  9946. /// set if optInterceptInputOutput is defined in TServiceFactoryServer.Options
  9947. // - this is a read-only property: you cannot change the output content
  9948. // - contains a dvObject with output parameters as "argname":value pairs
  9949. property Output: TDocVariantData read fOutput;
  9950. /// set if intercepted event Step is smsError
  9951. property LastException: Exception read fLastException;
  9952. /// reference to the background execution thread, if any
  9953. property BackgroundExecutionThread: TSynBackgroundThreadMethod
  9954. read fBackgroundExecutionThread;
  9955. /// points e.g. to TSQLRestServerURIContext.ExecuteCallback
  9956. property OnCallback: TServiceMethodExecuteCallback read fOnCallback;
  9957. /// allow to use an instance-specific temporary TTextWriter
  9958. function TempTextWriter: TJSONSerializer;
  9959. end;
  9960. /// a record type to be used as result for a function method for custom content
  9961. // for interface-based services
  9962. // - all answers are pure JSON object by default: using this kind of record
  9963. // as result will allow a response of any type (e.g. binary, HTML or text)
  9964. // - this kind of answer will be understood by our TServiceContainerClient
  9965. // implementation, and it may be used with plain AJAX or HTML requests
  9966. // (via POST), to retrieve some custom content
  9967. TServiceCustomAnswer = record
  9968. /// mandatory response type, as encoded in the HTTP header
  9969. // - useful to set the response mime-type - see e.g. the
  9970. // TEXT_CONTENT_TYPE_HEADER or HTML_CONTENT_TYPE_HEADER constants or
  9971. // GetMimeContentType() function
  9972. // - in order to be handled as expected, this field SHALL be set to NOT ''
  9973. // (otherwise TServiceCustomAnswer will be transmitted as raw JSON)
  9974. Header: RawUTF8;
  9975. /// the response body
  9976. // - corresponding to the response type, as defined in Header
  9977. Content: RawByteString;
  9978. /// the HTML response code
  9979. // - if not overriden, will default to HTML_SUCCESS = 200 on server side
  9980. // - on client side, would always contain HTML_SUCCESS = 200 on success,
  9981. // or any error should be handled as expected by the caller (e.g. using
  9982. // TServiceFactoryClient.GetErrorMessage for decoding REST/SOA errors)
  9983. Status: cardinal;
  9984. end;
  9985. PServiceCustomAnswer = ^TServiceCustomAnswer;
  9986. {$M+}
  9987. /// abstract factory class allowing to call interface resolution in cascade
  9988. // - you can inherit from this class to chain the TryResolve() calls so
  9989. // that several kind of implementations may be asked by a TInjectableObject,
  9990. // e.g. TInterfaceStub, TServiceContainer or TDDDRepositoryRestObjectMapping
  9991. // - this will implement factory pattern, as a safe and thread-safe DI/IoC
  9992. TInterfaceResolver = class
  9993. protected
  9994. /// override this method to resolve an interface from this instance
  9995. function TryResolve(aInterface: PTypeInfo; out Obj): boolean; virtual; abstract;
  9996. /// override this method check if this instance implements aInterface
  9997. function Implements(aInterface: PTypeInfo): boolean; virtual; abstract;
  9998. end;
  9999. {$M-}
  10000. /// abstract factory class targetting a single kind of interface
  10001. TInterfaceResolverForSingleInterface = class(TInterfaceResolver)
  10002. protected
  10003. fInterfaceTypeInfo: PTypeInfo;
  10004. fInterfaceAncestors: PTypeInfoDynArray;
  10005. fInterfaceAncestorsImplementationEntry: TPointerDynArray;
  10006. fImplementationEntry: PInterfaceEntry;
  10007. fImplementation: TClassInstance;
  10008. function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override;
  10009. function Implements(aInterface: PTypeInfo): boolean; override;
  10010. function GetImplementationName: string;
  10011. // main IoC/DI virtual method - call fImplementation.CreateNew by default
  10012. function CreateInstance: TInterfacedObject; virtual;
  10013. public
  10014. /// this overriden constructor will check and store the supplied class
  10015. // to implement an interface
  10016. constructor Create(aInterface: PTypeInfo; aImplementation: TInterfacedObjectClass); overload;
  10017. /// this overriden constructor will check and store the supplied class
  10018. // to implement an interface by TGUID
  10019. constructor Create(const aInterface: TGUID; aImplementation: TInterfacedObjectClass); overload;
  10020. /// you can use this method to resolve the interface as a new instance
  10021. function GetOneInstance(out Obj): boolean;
  10022. published
  10023. /// the class name which will implement each repository instance
  10024. property ImplementationClass: string read GetImplementationName;
  10025. end;
  10026. TInterfaceStub = class;
  10027. /// used to store a list of TInterfacedObject instances
  10028. TInterfacedObjectObjArray = array of TInterfacedObject;
  10029. /// used to store a list of TInterfaceResolver instances
  10030. TInterfaceResolverObjArray = array of TInterfaceResolver;
  10031. /// used to store a list of TInterfaceStub instances
  10032. TInterfaceStubObjArray = array of TInterfaceStub;
  10033. /// abstract factory class targetting any kind of interface
  10034. // - you can inherit from this class to customize dependency injection (DI/IoC),
  10035. // defining the resolution via InjectStub/InjectResolver/InjectInstance methods,
  10036. // and doing the instance resolution using the overloaded Resolve*() methods
  10037. // - TServiceContainer will inherit from this class, as the main entry point
  10038. // for interface-based services of the framework (via TSQLRest.Services)
  10039. // - you can use RegisterGlobal() class method to define some process-wide DI
  10040. TInterfaceResolverInjected = class(TInterfaceResolver)
  10041. protected
  10042. fResolvers: TInterfaceResolverObjArray;
  10043. fResolversToBeReleased: TInterfaceResolverObjArray;
  10044. fDependencies: TInterfacedObjectObjArray;
  10045. function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override;
  10046. function TryResolveInternal(aInterface: PTypeInfo; out Obj): boolean;
  10047. function Implements(aInterface: PTypeInfo): boolean; override;
  10048. class function RegisterGlobalCheck(aInterface: PTypeInfo;
  10049. aImplementationClass: TClass): PInterfaceEntry;
  10050. public
  10051. /// define a global class type for interface resolution
  10052. // - most of the time, you would need a local DI/IoC resolution list; but
  10053. // you may use this method to register a set of shared and global resolution
  10054. // patterns, common to the whole injection process
  10055. // - by default, TAutoLocker and TLockedDocVariant will be registered by
  10056. // this unit to implement IAutoLocker and ILockedDocVariant interfaces
  10057. class procedure RegisterGlobal(aInterface: PTypeInfo;
  10058. aImplementationClass: TInterfacedObjectWithCustomCreateClass); overload;
  10059. /// define a global instance for interface resolution
  10060. // - most of the time, you would need a local DI/IoC resolution list; but
  10061. // you may use this method to register a set of shared and global resolution
  10062. // patterns, common to the whole injection process
  10063. // - the supplied instance will be owned by the global list (incrementing
  10064. // its internal reference count), until it would be released via
  10065. // ! RegisterGlobalDelete()
  10066. // - the supplied instance will be freed in the finalization of this unit,
  10067. // if not previously released via RegisterGlobalDelete()
  10068. class procedure RegisterGlobal(aInterface: PTypeInfo;
  10069. aImplementation: TInterfacedObject); overload;
  10070. /// undefine a global instance for interface resolution
  10071. // - you can unregister a given instance previously defined via
  10072. // ! RegisterGlobal(aInterface,aImplementation)
  10073. // - if you do not call RegisterGlobalDelete(), the remaning instances will
  10074. // be freed in the finalization of this unit
  10075. class procedure RegisterGlobalDelete(aInterface: PTypeInfo);
  10076. /// prepare and setup interface DI/IoC resolution with some blank
  10077. // TInterfaceStub specified by their TGUID
  10078. procedure InjectStub(const aStubsByGUID: array of TGUID); overload; virtual;
  10079. /// prepare and setup interface DI/IoC resolution with TInterfaceResolver
  10080. // kind of factory
  10081. // - e.g. a customized TInterfaceStub/TInterfaceMock, a TServiceContainer,
  10082. // a TDDDRepositoryRestObjectMapping or any factory class
  10083. // - by default, only TInterfaceStub/TInterfaceMock would be owned by this
  10084. // instance, and released by Destroy - unless you set OwnOtherResolvers
  10085. procedure InjectResolver(const aOtherResolvers: array of TInterfaceResolver;
  10086. OwnOtherResolvers: boolean=false); overload; virtual;
  10087. /// prepare and setup interface DI/IoC resolution from a TInterfacedObject instance
  10088. // - any TInterfacedObject declared as dependency will have its reference
  10089. // count increased, and decreased in Destroy
  10090. procedure InjectInstance(const aDependencies: array of TInterfacedObject); overload; virtual;
  10091. /// can be used to perform an DI/IoC for a given interface
  10092. // - will search for the supplied interface to its internal list of resolvers
  10093. // - returns TRUE and set the Obj variable with a matching instance
  10094. // - can be used as such to resolve an ICalculator interface:
  10095. // ! var calc: ICalculator;
  10096. // ! begin
  10097. // ! if Catalog.Resolve(TypeInfo(ICalculator),calc) then
  10098. // ! ... use calc methods
  10099. function Resolve(aInterface: PTypeInfo; out Obj): boolean; overload;
  10100. /// can be used to perform an DI/IoC for a given interface
  10101. // - you shall have registered the interface TGUID by a previous call to
  10102. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(ICalculator),...])
  10103. // - returns TRUE and set the Obj variable with a matching instance
  10104. // - can be used as such to resolve an ICalculator interface:
  10105. // ! var calc: ICalculator;
  10106. // ! begin
  10107. // ! if ServiceContainer.Resolve(ICalculator,cal) then
  10108. // ! ... use calc methods
  10109. function Resolve(const aGUID: TGUID; out Obj): boolean; overload;
  10110. /// can be used to perform several DI/IoC for a given set of interfaces
  10111. // - here interfaces and instances are provided as TypeInfo,@Instance pairs
  10112. // - raise an EServiceException if any interface can't be resolved, unless
  10113. // aRaiseExceptionIfNotFound is set to FALSE
  10114. procedure ResolveByPair(const aInterfaceObjPairs: array of pointer;
  10115. aRaiseExceptionIfNotFound: boolean=true);
  10116. /// can be used to perform several DI/IoC for a given set of interfaces
  10117. // - here interfaces and instances are provided as TGUID and @Instance
  10118. // - you shall have registered the interface TGUID by a previous call to
  10119. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(ICalculator),...])
  10120. // - raise an EServiceException if any interface can't be resolved, unless
  10121. // aRaiseExceptionIfNotFound is set to FALSE
  10122. procedure Resolve(const aInterfaces: array of TGUID; const aObjs: array of pointer;
  10123. aRaiseExceptionIfNotFound: boolean=true); overload;
  10124. /// release all used instances
  10125. // - including all TInterfaceStub instances as specified to Inject(aStubsByGUID)
  10126. // - will call _Release on all TInterfacedObject dependencies
  10127. destructor Destroy; override;
  10128. end;
  10129. /// any service implementation class could inherit from this class to
  10130. // allow dependency injection aka SOLID DI/IoC by the framework
  10131. // - once created, the framework will call AddResolver() member, so that its
  10132. // Resolve*() methods could be used to inject any needed dependency for lazy
  10133. // dependency resolution (e.g. within a public property getter)
  10134. // - any interface published property would also be automatically injected
  10135. // - if you implement a SOA service with this class, TSQLRestServer.Services
  10136. // will be auto-injected via TServiceFactoryServer.CreateInstance()
  10137. TInjectableObject = class(TInterfacedObjectWithCustomCreate)
  10138. protected
  10139. fResolver: TInterfaceResolver;
  10140. fResolverOwned: Boolean;
  10141. // DI/IoC resolution protected methods
  10142. function TryResolve(aInterface: PTypeInfo; out Obj): boolean;
  10143. /// this method will resolve all interface published properties
  10144. procedure AutoResolve(aRaiseEServiceExceptionIfNotFound: boolean);
  10145. public
  10146. /// initialize an instance, defining one or several mean of dependency resolution
  10147. // - simple TInterfaceStub could be created directly from their TGUID,
  10148. // then any kind of DI/IoC resolver instances could be specified, i.e.
  10149. // either customized TInterfaceStub/TInterfaceMock, a TServiceContainer or
  10150. // a TDDDRepositoryRestObjectMapping, and then any TInterfacedObject
  10151. // instance would be used during dependency resolution:
  10152. // ! procedure TMyTestCase.OneTestCaseMethod;
  10153. // ! var Test: IServiceToBeTested;
  10154. // ! begin
  10155. // ! Test := TServiceToBeTested.CreateInjected(
  10156. // ! [ICalculator],
  10157. // ! [TInterfaceMock.Create(IPersistence,self).
  10158. // ! ExpectsCount('SaveItem',qoEqualTo,1),
  10159. // ! RestInstance.Services],
  10160. // ! [AnyInterfacedObject]);
  10161. // ! ...
  10162. // - note that all the injected stubs/mocks instances will be owned by the
  10163. // TInjectableObject, and therefore released with it
  10164. // - any TInterfacedObject declared as dependency will have its reference
  10165. // count increased, and decreased in Destroy
  10166. // - once DI/IoC is defined, will call the AutoResolve() protected method
  10167. constructor CreateInjected(const aStubsByGUID: array of TGUID;
  10168. const aOtherResolvers: array of TInterfaceResolver;
  10169. const aDependencies: array of TInterfacedObject;
  10170. aRaiseEServiceExceptionIfNotFound: boolean=true); virtual;
  10171. /// initialize an instance, defining one dependency resolver
  10172. // - the resolver may be e.g. a TServiceContainer
  10173. // - once the DI/IoC is defined, will call the AutoResolve() protected method
  10174. constructor CreateWithResolver(aResolver: TInterfaceResolver;
  10175. aRaiseEServiceExceptionIfNotFound: boolean=true); virtual;
  10176. /// can be used to perform an DI/IoC for a given interface type information
  10177. procedure Resolve(aInterface: PTypeInfo; out Obj); overload;
  10178. /// can be used to perform an DI/IoC for a given interface TGUID
  10179. procedure Resolve(const aGUID: TGUID; out Obj); overload;
  10180. /// can be used to perform several DI/IoC for a given set of interfaces
  10181. // - here interfaces and instances are provided as TypeInfo,@Instance pairs
  10182. procedure ResolveByPair(const aInterfaceObjPairs: array of pointer);
  10183. /// can be used to perform several DI/IoC for a given set of interfaces
  10184. // - here interfaces and instances are provided as TGUID and pointers
  10185. procedure Resolve(const aInterfaces: array of TGUID; const aObjs: array of pointer); overload;
  10186. /// release all used instances
  10187. // - including all TInterfaceStub instances as specified to CreateInjected()
  10188. destructor Destroy; override;
  10189. /// access to the associated dependency resolver, if any
  10190. property Resolver: TInterfaceResolver read fResolver;
  10191. end;
  10192. /// class-reference type (metaclass) of a TInjectableObject type
  10193. TInjectableObjectClass = class of TInjectableObject;
  10194. /// service implementation class, with direct access on the associated
  10195. // TServiceFactoryServer/TSQLRestServer instances
  10196. // - allow dependency injection aka SOLID DI/IoC by the framework using
  10197. // inherited TInjectableObject.Resolve() methods
  10198. // - allows direct access to the underlying ORM using its Server method
  10199. // - this class would allow Server instance access outside the scope of
  10200. // remote SOA execution, e.g. when a DI is performed on server side: it
  10201. // is therefore a better alternative to ServiceContext.Factory,
  10202. // ServiceContext.Factory.RestServer or ServiceContext.Request.Server
  10203. TInjectableObjectRest = class(TInjectableObject)
  10204. protected
  10205. fFactory: TServiceFactoryServer;
  10206. fServer: TSQLRestServer;
  10207. public
  10208. /// access to the associated interface factory
  10209. // - this property will be injected by TServiceFactoryServer.CreateInstance,
  10210. // so may be nil if the instance was created outside the SOA context
  10211. property Factory: TServiceFactoryServer read fFactory;
  10212. /// access ot the associated REST Server, e.g. to its ORM methods
  10213. // - slightly faster than Factory.RestServer
  10214. // - this value will be injected by TServiceFactoryServer.CreateInstance,
  10215. // so may be nil if the instance was created outside the SOA context
  10216. property Server: TSQLRestServer read fServer;
  10217. end;
  10218. /// used to set the published properties of a TInjectableAutoCreateFields
  10219. // - TInjectableAutoCreateFields.Create will check any resolver able to
  10220. // implement this interface, then run its SetProperties() method on it
  10221. IAutoCreateFieldsResolve = interface
  10222. ['{396362E9-B60D-43D4-A0D4-802E4479F24E}']
  10223. /// this method will be called once on any TInjectableAutoCreateFields just
  10224. // created instance
  10225. procedure SetProperties(Instance: TObject);
  10226. end;
  10227. /// abstract class which will auto-inject its dependencies (DI/IoC), and also
  10228. // manage the instances of its TPersistent/TSynPersistent published properties
  10229. // - abstract class able with a virtual constructor, dependency injection
  10230. // (i.e. SOLID DI/IoC), and automatic memory management of all nested class
  10231. // published properties
  10232. // - will also release any T*ObjArray dynamic array storage of persistents,
  10233. // previously registered via TJSONSerializer.RegisterObjArrayForJSON()
  10234. // - this class is a perfect parent for any class storing data by value, and
  10235. // dependency injection, e.g. DDD services or daemons
  10236. // - note that non published (e.g. public) properties won't be instantiated
  10237. // - please take care that you would not create any endless recursion: you
  10238. // should ensure that at one level, nested published properties won't have any
  10239. // class instance matching its parent type
  10240. // - since the destructor will release all nested properties, you should
  10241. // never store a reference of any of those nested instances outside
  10242. // - if any associated resolver implements IAutoCreateFieldsResolve, its
  10243. // SetProperties() method will be called on all created T*Persistent
  10244. // published properties, so that it may initialize its values
  10245. TInjectableAutoCreateFields = class(TInjectableObject)
  10246. public
  10247. /// this overriden constructor will instantiate all its nested
  10248. // TPersistent/TSynPersistent/TSynAutoCreateFields class published properties
  10249. // - then resolve and call IAutoCreateFieldsResolve.SetProperties(self)
  10250. constructor Create; override;
  10251. /// finalize the instance, and release its published properties
  10252. destructor Destroy; override;
  10253. end;
  10254. /// event used by TInterfaceFactory to run a method from a fake instance
  10255. // - aMethod will specify which method is to be executed
  10256. // - aParams will contain the input parameters, encoded as a JSON array
  10257. // - shall return TRUE on success, or FALSE in case of failure, with
  10258. // a corresponding explanation in aErrorMsg
  10259. // - method results shall be serialized as JSON in aResult; if
  10260. // aServiceCustomAnswer is not nil, the result shall use this record
  10261. // to set HTTP custom content and headers, and ignore aResult content
  10262. // - aClientDrivenID can be set optionally to specify e.g. an URI-level session
  10263. TOnFakeInstanceInvoke = function (const aMethod: TServiceMethod;
  10264. const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8;
  10265. aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean of object;
  10266. /// event called when destroying a TInterfaceFactory's fake instance
  10267. /// - this method will be run when the fake class instance is destroyed
  10268. // (e.g. if aInstanceCreation is sicClientDriven, to notify the server
  10269. // than the client life time just finished)
  10270. TOnFakeInstanceDestroy = procedure(aClientDrivenID: cardinal) of object;
  10271. /// may be used to store the Methods[] indexes of a TInterfaceFactory
  10272. TInterfaceFactoryMethodBits = set of 0..255;
  10273. /// a dynamic array of TInterfaceFactory instances
  10274. TInterfaceFactoryObjArray = array of TInterfaceFactory;
  10275. /// class handling interface RTTI and fake implementation class
  10276. // - a thread-safe global list of such class instances is implemented to cache
  10277. // information for better speed: use class function TInterfaceFactory.Get()
  10278. // and not manual TInterfaceFactory.Create / Free
  10279. // - if you want to search the interfaces by name or TGUID, call once
  10280. // Get(TypeInfo(IMyInterface)) or RegisterInterfaces() for proper registration
  10281. // - will use TInterfaceFactoryRTTI classes generated from Delphi RTTI
  10282. TInterfaceFactory = class
  10283. protected
  10284. fInterfaceTypeInfo: PTypeInfo;
  10285. fInterfaceIID: TGUID;
  10286. fMethodsCount: cardinal;
  10287. fAddMethodsLevel: integer;
  10288. fMethods: TServiceMethodDynArray;
  10289. fMethod: TDynArrayHashed;
  10290. // contains e.g. [{"method":"Add","arguments":[...]},{"method":"...}]
  10291. fContract: RawUTF8;
  10292. fInterfaceName: RawUTF8;
  10293. {$ifndef NOVARIANTS}
  10294. fDocVariantOptions: TDocVariantOptions;
  10295. {$endif}
  10296. fFakeVTable: array of pointer;
  10297. fFakeStub: PByteArray;
  10298. fMethodIndexCallbackReleased: Integer;
  10299. fMethodIndexCurrentFrameCallback: Integer;
  10300. {$ifdef CPUAARCH64}
  10301. fDetectX0ResultMagic: cardinal; // alf: temporary hack for AARCH64
  10302. {$endif}
  10303. procedure AddMethodsFromTypeInfo(aInterface: PTypeInfo); virtual; abstract;
  10304. function GetMethodsVirtualTable: pointer;
  10305. public
  10306. /// this is the main entry point to the global interface factory cache
  10307. // - access to this method is thread-safe
  10308. // - this method will also register the class to further retrieval
  10309. class function Get(aInterface: PTypeInfo): TInterfaceFactory; overload;
  10310. /// retrieve an interface factory from cache, from its TGUID
  10311. // - access to this method is thread-safe
  10312. // - you shall have registered the interface by a previous call to the
  10313. // overloaded Get(TypeInfo(IMyInterface)) method or RegisterInterfaces()
  10314. // - if the supplied TGUID has not been previously registered, returns nil
  10315. class function Get(const aGUID: TGUID): TInterfaceFactory; overload;
  10316. /// retrieve an interface factory from cache, from its name (e.g. 'IMyInterface')
  10317. // - access to this method is thread-safe
  10318. // - you shall have registered the interface by a previous call to the
  10319. // overloaded Get(TypeInfo(IMyInterface)) method or RegisterInterfaces()
  10320. // - if the supplied TGUID has not been previously registered, returns nil
  10321. class function Get(const aInterfaceName: RawUTF8): TInterfaceFactory; overload;
  10322. /// register one or several interfaces to the global interface factory cache
  10323. // - so that you can use TInterfaceFactory.Get(aGUID) or Get(aName)
  10324. class procedure RegisterInterfaces(const aInterfaces: array of PTypeInfo);
  10325. /// could be used to retrieve an array of TypeInfo() from their GUID
  10326. class function GUID2TypeInfo(const aGUIDs: array of TGUID): PTypeInfoDynArray; overload;
  10327. /// could be used to retrieve an array of TypeInfo() from their GUID
  10328. class function GUID2TypeInfo(const aGUID: TGUID): PTypeInfo; overload;
  10329. /// returns the list of all declared TInterfaceFactory
  10330. // - as used by SOA and mocking/stubing features of this unit
  10331. class function GetUsedInterfaces: TObjectList;
  10332. /// add some TInterfaceFactory instances from their GUID
  10333. class procedure AddToObjArray(var Obj: TInterfaceFactoryObjArray;
  10334. const aGUIDs: array of TGUID);
  10335. /// initialize the internal properties from the supplied interface RTTI
  10336. // - it will check and retrieve all methods of the supplied interface,
  10337. // and prepare all internal structures for later use
  10338. // - do not call this constructor directly, but TInterfaceFactory.Get()
  10339. constructor Create(aInterface: PTypeInfo);
  10340. /// find the index of a particular method in internal Methods[] list
  10341. // - will search for a match against Methods[].URI property
  10342. // - won't find the default AddRef/Release/QueryInterface methods
  10343. // - will return -1 if the method is not known
  10344. // - if aMethodName does not have an exact method match, it would try with a
  10345. // trailing underscore, so that e.g. /service/start would match IService._Start()
  10346. function FindMethodIndex(const aMethodName: RawUTF8): integer;
  10347. /// find the index of a particular interface.method in internal Methods[] list
  10348. // - will search for a match against Methods[].InterfaceDotMethodName property
  10349. // - won't find the default AddRef/Release/QueryInterface methods
  10350. // - will return -1 if the method is not known
  10351. function FindFullMethodIndex(const aFullMethodName: RawUTF8;
  10352. alsoSearchExactMethodName: boolean=false): integer;
  10353. /// find the index of a particular method in internal Methods[] list
  10354. // - won't find the default AddRef/Release/QueryInterface methods
  10355. // - will raise an EInterfaceFactoryException if the method is not known
  10356. function CheckMethodIndex(const aMethodName: RawUTF8): integer; overload;
  10357. /// find the index of a particular method in internal Methods[] list
  10358. // - won't find the default AddRef/Release/QueryInterface methods
  10359. // - will raise an EInterfaceFactoryException if the method is not known
  10360. function CheckMethodIndex(aMethodName: PUTF8Char): integer; overload;
  10361. /// returns the method name from its method index
  10362. // - the method index should start at 0 for _free_/_contract_/_signature_
  10363. // pseudo-methods, and start at index 3 for real Methods[]
  10364. function GetMethodName(MethodIndex: integer): RawUTF8;
  10365. /// set the Methods[] indexes bit from some methods names
  10366. // - won't find the default AddRef/Release/QueryInterface methods
  10367. // - will raise an EInterfaceFactoryException if the method is not known
  10368. procedure CheckMethodIndexes(const aMethodName: array of RawUTF8; aSetAllIfNone: boolean;
  10369. out aBits: TInterfaceFactoryMethodBits);
  10370. /// returns the full 'Interface.MethodName' text, from a method index
  10371. // - the method index should start at 0 for _free_/_contract_/_signature_
  10372. // pseudo-methods, and start at index 3 for real Methods[]
  10373. // - will return plain 'Interface' text, if aMethodIndex is incorrect
  10374. function GetFullMethodName(aMethodIndex: integer): RawUTF8;
  10375. /// the declared internal methods
  10376. // - list does not contain default AddRef/Release/QueryInterface methods
  10377. // - nor the _free_/_contract_/_signature_ pseudo-methods
  10378. property Methods: TServiceMethodDynArray read fMethods;
  10379. /// the number of internal methods
  10380. // - does not include the default AddRef/Release/QueryInterface methods
  10381. // - nor the _free_/_contract_/_signature_ pseudo-methods
  10382. property MethodsCount: cardinal read fMethodsCount;
  10383. /// identifies a CallbackReleased() method in this interface
  10384. // - i.e. the index in Methods[] of the following signature:
  10385. // ! procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8);
  10386. // - this method will be called e.g. by TInterfacedCallback.Destroy, when
  10387. // a callback is released on the client side so that you may be able e.g. to
  10388. // unsubscribe the callback from an interface list (via InterfaceArrayDelete)
  10389. // - contains -1 if no such method do exist in the interface definition
  10390. property MethodIndexCallbackReleased: Integer read fMethodIndexCallbackReleased;
  10391. /// identifies a CurrentFrame() method in this interface
  10392. // - i.e. the index in Methods[] of the following signature:
  10393. // ! procedure CurrentFrame(isLast: boolean);
  10394. // - this method will be called e.g. by TSQLHttpClientWebsockets.CallbackRequest
  10395. // for interface callbacks in case of WebSockets jumbo frames, to allow e.g.
  10396. // faster database access via a batch
  10397. // - contains -1 if no such method do exist in the interface definition
  10398. property MethodIndexCurrentFrameCallback: Integer read fMethodIndexCurrentFrameCallback;
  10399. /// the registered Interface low-level Delphi RTTI type
  10400. property InterfaceTypeInfo: PTypeInfo read fInterfaceTypeInfo;
  10401. /// the registered Interface GUID
  10402. property InterfaceIID: TGUID read fInterfaceIID;
  10403. {$ifndef NOVARIANTS}
  10404. /// how this interface will work with variants (including TDocVariant)
  10405. // - by default, contains JSON_OPTIONS_FAST for best performance - i.e.
  10406. // [dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]
  10407. property DocVariantOptions: TDocVariantOptions
  10408. read fDocVariantOptions write fDocVariantOptions;
  10409. {$endif}
  10410. published
  10411. /// will return the interface name, e.g. 'ICalculator'
  10412. // - published property to be serializable as JSON e.g. for debbuging info
  10413. property InterfaceName: RawUTF8 read fInterfaceName;
  10414. end;
  10415. {$ifdef HASINTERFACERTTI}
  10416. /// class handling interface RTTI and fake implementation class
  10417. // - this class only exists for Delphi 6 and up, since FPC does not generate
  10418. // the expected RTTI - see http://bugs.freepascal.org/view.php?id=26774
  10419. TInterfaceFactoryRTTI = class(TInterfaceFactory)
  10420. protected
  10421. procedure AddMethodsFromTypeInfo(aInterface: PTypeInfo); override;
  10422. end;
  10423. {$endif HASINTERFACERTTI}
  10424. {$M+}
  10425. /// how TInterfacedObjectFromFactory would perform its execution
  10426. // - by default, fInvoke() would receive standard JSON content, unless
  10427. // ifoJsonAsExtended is set, and extended JSON is used
  10428. TInterfacedObjectFromFactoryOption = (ifoJsonAsExtended);
  10429. /// defines how TInterfacedObjectFromFactory would perform its execution
  10430. TInterfacedObjectFromFactoryOptions = set of TInterfacedObjectFromFactoryOption;
  10431. /// abstract class handling a generic interface implementation class
  10432. TInterfacedObjectFromFactory = class(TInterfacedObject)
  10433. protected
  10434. fFactory: TInterfaceFactory;
  10435. fOptions: TInterfacedObjectFromFactoryOptions;
  10436. fInvoke: TOnFakeInstanceInvoke;
  10437. fNotifyDestroy: TOnFakeInstanceDestroy;
  10438. fClientDrivenID: Cardinal;
  10439. public
  10440. /// create an instance, using the specified interface
  10441. constructor Create(aFactory: TInterfaceFactory;
  10442. aOptions: TInterfacedObjectFromFactoryOptions;
  10443. aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy);
  10444. /// release the remote server instance (in sicClientDriven mode);
  10445. destructor Destroy; override;
  10446. published
  10447. /// the associated interface factory class
  10448. property Factory: TInterfaceFactory read fFactory;
  10449. /// the ID used in sicClientDriven mode
  10450. property ClientDrivenID: Cardinal read fClientDrivenID;
  10451. end;
  10452. {$M-}
  10453. /// class handling interface implementation generated from source
  10454. // - this class targets FPC, which does not generate the expected RTTI - see
  10455. // http://bugs.freepascal.org/view.php?id=26774
  10456. // - mORMotWrapper.pas will generate a new inherited class, overriding abstract
  10457. // AddMethodsFromTypeInfo() to define the interface methods
  10458. TInterfaceFactoryGenerated = class(TInterfaceFactory)
  10459. protected
  10460. fTempStrings: TRawUTF8DynArray;
  10461. /// the overriden AddMethodsFromTypeInfo() method will call e.g. as
  10462. // ! AddMethod('Add',[
  10463. // ! 0,'n1',TypeInfo(Integer),
  10464. // ! 0,'n2',TypeInfo(Integer),
  10465. // ! 3,'Result',TypeInfo(Integer)]);
  10466. // with 0=ord(smdConst) and 3=ord(smdResult)
  10467. procedure AddMethod(const aName: RawUTF8; const aParams: array of const); virtual;
  10468. public
  10469. /// register one interface type definition from the current class
  10470. // - will be called by mORMotWrapper.pas generated code, in initialization
  10471. // section, so that the needed type information would be available
  10472. class procedure RegisterInterface(aInterface: PTypeInfo); virtual;
  10473. end;
  10474. /// abstract parameters used by TInterfaceStub.Executes() events callbacks
  10475. TOnInterfaceStubExecuteParamsAbstract = class
  10476. private
  10477. function GetSenderAsMockTestCase: TSynTestCase;
  10478. protected
  10479. fSender: TInterfaceStub;
  10480. fMethod: PServiceMethod;
  10481. fParams: RawUTF8;
  10482. fEventParams: RawUTF8;
  10483. fResult: RawUTF8;
  10484. fFailed: boolean;
  10485. public
  10486. /// constructor of one parameters marshalling instance
  10487. constructor Create(aSender: TInterfaceStub; aMethod: PServiceMethod;
  10488. const aParams,aEventParams: RawUTF8); virtual;
  10489. /// call this method if the callback implementation failed
  10490. procedure Error(const aErrorMessage: RawUTF8); overload;
  10491. /// call this method if the callback implementation failed
  10492. procedure Error(const Format: RawUTF8; const Args: array of const); overload;
  10493. /// the stubbing / mocking generator
  10494. property Sender: TInterfaceStub read fSender;
  10495. /// the mocking generator associated test case
  10496. // - will raise an exception if the associated Sender generator is not
  10497. // a TInterfaceMock
  10498. property TestCase: TSynTestCase read GetSenderAsMockTestCase;
  10499. /// pointer to the method which is to be executed
  10500. property Method: PServiceMethod read fMethod;
  10501. /// a custom message, defined at TInterfaceStub.Executes() definition
  10502. property EventParams: RawUTF8 read fEventParams;
  10503. /// outgoing values array encoded as JSON
  10504. // - every var, out parameter or the function result shall be encoded as
  10505. // a JSON array into this variable, in the same order than the stubbed
  10506. // method declaration
  10507. // - use Returns() method to create the JSON array directly, from an array
  10508. // of values
  10509. property Result: RawUTF8 read fResult;
  10510. /// low-level flag, set to TRUE if one of the Error() method was called
  10511. property Failed: boolean read fFailed;
  10512. end;
  10513. {$ifndef NOVARIANTS}
  10514. /// parameters used by TInterfaceStub.Executes() events callbacks as Variant
  10515. // - this class will expect input and output parameters to specified as
  10516. // variant arrays properties, so is easier (and a bit slower) than the
  10517. // TOnInterfaceStubExecuteParamsJSON class
  10518. TOnInterfaceStubExecuteParamsVariant = class(TOnInterfaceStubExecuteParamsAbstract)
  10519. private
  10520. function GetInput(Index: Integer): variant;
  10521. procedure SetOutput(Index: Integer; const Value: variant);
  10522. function GetInNamed(const aParamName: RawUTF8): variant;
  10523. procedure SetOutNamed(const aParamName: RawUTF8; const Value: variant);
  10524. protected
  10525. fInput: TVariantDynArray;
  10526. fOutput: TVariantDynArray;
  10527. procedure SetResultFromOutput;
  10528. public
  10529. /// constructor of one parameters marshalling instance
  10530. constructor Create(aSender: TInterfaceStub; aMethod: PServiceMethod;
  10531. const aParams,aEventParams: RawUTF8); override;
  10532. /// returns the input parameters as a TDocVariant object or array
  10533. function InputAsDocVariant(Kind: TServiceMethodParamsDocVariantKind;
  10534. Options: TDocVariantOptions=[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]): variant;
  10535. /// returns the output parameters as a TDocVariant object or array
  10536. function OutputAsDocVariant(Kind: TServiceMethodParamsDocVariantKind;
  10537. Options: TDocVariantOptions=[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]): variant;
  10538. /// input parameters when calling the method
  10539. // - order shall follow the method const and var parameters
  10540. // ! Stub.Add(10,20) -> Input[0]=10, Input[1]=20
  10541. // - if the supplied Index is out of range, an EInterfaceStub will be raised
  10542. property Input[Index: Integer]: variant read GetInput;
  10543. /// output parameters returned after method process
  10544. // - order shall follow the method var, out parameters and the function
  10545. // result (if method is not a procedure)
  10546. // - if the supplied Index is out of range, an EInterfaceStub will be raised
  10547. // - can be used as such:
  10548. // ! procedure TFooTestCase.ExecuteBar(Ctxt: TOnInterfaceStubExecuteParamsVariant);
  10549. // ! begin // Input[0]=i
  10550. // ! Ctxt.Output[0] := Ctxt.Input[0]+1; // i := i+1;
  10551. // ! Ctxt.Output[1] := 42; // result := 42;
  10552. // ! end; // Output|0]=i, Output[1]=result
  10553. // to emulate this native implementation:
  10554. // ! function Bar(var i: Integer): Integer;
  10555. // ! begin
  10556. // ! inc(i);
  10557. // ! result := 42;
  10558. // ! end;
  10559. // - consider using the safest Named[] property, to avoid parameters
  10560. // index matching issue
  10561. // - if an Output[]/Named[] item is not set, a default value would be used
  10562. property Output[Index: Integer]: variant write SetOutput;
  10563. /// access to input/output parameters when calling the method
  10564. // - if the supplied name is incorrect, an EInterfaceStub will be raised
  10565. // - is a bit slower than Input[]/Output[] indexed properties, but easier
  10566. // to work with, and safer in case of method signature change (like parameter
  10567. // add or rename)
  10568. // - marked as default property, so you can use it e.g. as such:
  10569. // ! procedure TFooTestCase.ExecuteBar(Ctxt: TOnInterfaceStubExecuteParamsVariant);
  10570. // ! begin
  10571. // ! Ctxt['i'] := Ctxt['i']+1; // i := i+1;
  10572. // ! Ctxt['result'] := 42; // result := 42;
  10573. // ! end;
  10574. // to emulate this native implementation:
  10575. // ! function Bar(var i: Integer): Integer;
  10576. // ! begin
  10577. // ! inc(i);
  10578. // ! result := 42;
  10579. // ! end;
  10580. // - using this default Named[] property is recommended over the index-based
  10581. // Output[] property
  10582. // - if an Output[]/Named[] item is not set, a default value would be used
  10583. property Named[const ParamName: RawUTF8]: variant read GetInNamed write SetOutNamed; default;
  10584. end;
  10585. {$endif NOVARIANTS}
  10586. /// parameters used by TInterfaceStub.Executes() events callbacks as JSON
  10587. // - this class will expect input and output parameters to be encoded as
  10588. // JSON arrays, so is faster than TOnInterfaceStubExecuteParamsVariant
  10589. TOnInterfaceStubExecuteParamsJSON = class(TOnInterfaceStubExecuteParamsAbstract)
  10590. public
  10591. /// a method to return an array of values into Result
  10592. // - just a wrapper around JSONEncodeArrayOfConst([...])
  10593. // - can be used as such:
  10594. // ! procedure TFooTestCase.ExecuteBar(var Ctxt: TOnInterfaceStubExecuteParamsJSON);
  10595. // ! begin // Ctxt.Params := '[i]' -> Ctxt.Result := '[i+1,42]'
  10596. // ! Ctxt.Returns([GetInteger(pointer(Ctxt.Params))+1,42]);
  10597. // ! end;
  10598. // to emulate this native implementation:
  10599. // ! function Bar(var i: Integer): Integer;
  10600. // ! begin
  10601. // ! inc(i);
  10602. // ! result := 42;
  10603. // ! end;
  10604. procedure Returns(const Values: array of const); overload;
  10605. /// a method to return a JSON array of values into Result
  10606. // - expected format is e.g. '[43,42]'
  10607. procedure Returns(const ValuesJsonArray: RawUTF8); overload;
  10608. /// incoming parameters array encoded as JSON array without braces
  10609. // - order follows the method const and var parameters
  10610. // ! Stub.Add(10,20) -> Params = '10,20';
  10611. property Params: RawUTF8 read fParams;
  10612. end;
  10613. {$ifndef NOVARIANTS}
  10614. /// event called by the TInterfaceStub.Executes() fluent method for variant process
  10615. // - by default Ctxt.Result shall contain the default JSON array result for
  10616. // this method - use Ctxt.Named[] default properties, e.g. as
  10617. // ! Ctxt['result'] := Ctxt['n1']-Ctxt['n2'];
  10618. // or with Input[] / Output[] properties:
  10619. // ! with Ctxt do Output[0] := Input[0]-Input[1];
  10620. // - you can call Ctxt.Error() to notify the caller for an execution error
  10621. TOnInterfaceStubExecuteVariant = procedure(Ctxt: TOnInterfaceStubExecuteParamsVariant) of object;
  10622. {$endif NOVARIANTS}
  10623. /// event called by the TInterfaceStub.Executes() fluent method for JSON process
  10624. // - by default Ctxt.Result shall contain the default JSON array result for
  10625. // this method - use Ctxt.Named[] default properties, e.g. as
  10626. // ! P := pointer(Ctxt.Params);
  10627. // ! Ctxt.Returns([GetNextItemDouble(P)-GetNextItemDouble(P)]);
  10628. // - you can call Ctxt.Error() to notify the caller for an execution error
  10629. TOnInterfaceStubExecuteJSON = procedure(Ctxt: TOnInterfaceStubExecuteParamsJSON) of object;
  10630. /// diverse types of stubbing / mocking rules
  10631. // - isUndefined is the first, since it will be a ExpectsCount() weak rule
  10632. // which may be overwritten by the other real run-time rules
  10633. TInterfaceStubRuleKind =
  10634. (isUndefined, isExecutesJSON, {$ifndef NOVARIANTS}isExecutesVariant, {$endif}
  10635. isRaises, isReturns, isFails);
  10636. /// define a mocking / stubing rule used internaly by TInterfaceStub
  10637. TInterfaceStubRule = record
  10638. /// optional expected parameters, serialized as a JSON array
  10639. // - if equals '', the rule is not parametrized - i.e. it will be the
  10640. // default for this method
  10641. Params: RawUTF8;
  10642. /// values associated to the rule
  10643. // - for TInterfaceStub.Executes(), is the aEventParams parameter transmitted
  10644. // to Execute event handler (could be used to e.g. customize the handler)
  10645. // - for TInterfaceStub.Raises(), is the Exception.Message associated
  10646. // to one ExceptionClass
  10647. // - for TInterfaceStub.Returns(), is the returned result, serialized as a
  10648. // JSON array (including var / out parameters then any function result)
  10649. // - for TInterfaceStub.Fails() is the returned error message for
  10650. // TInterfaceStub exception or TInterfaceMock associated test case
  10651. Values: RawUTF8;
  10652. /// the type of this rule
  10653. // - isUndefined is used for a TInterfaceStub.ExpectsCount() weak rule
  10654. Kind: TInterfaceStubRuleKind;
  10655. /// the event handler to be executed
  10656. // - for TInterfaceStub.Executes(), Values is transmitted as aResult parameter
  10657. // - either a TOnInterfaceStubExecuteJSON, or a TOnInterfaceStubExecuteVariant
  10658. Execute: TMethod;
  10659. /// the exception class to be raised
  10660. // - for TInterfaceStub.Raises(), Values contains Exception.Message
  10661. ExceptionClass: ExceptClass;
  10662. /// the number of times this rule has been executed
  10663. RulePassCount: cardinal;
  10664. /// comparison operator set by TInterfaceStub.ExpectsCount()
  10665. // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here
  10666. ExpectedPassCountOperator: TSQLQueryOperator;
  10667. /// expected pass count value set by TInterfaceStub.ExpectsCount()
  10668. // - value to be compared to the number of times this rule has been executed
  10669. // - TInterfaceStub/TInterfaceMock will check it in their Destroy destructor,
  10670. // using the comparison stated by ExpectedPassCountOperator
  10671. ExpectedPassCount: cardinal;
  10672. /// log trace value set by TInterfaceStub.ExpectsTrace()
  10673. // - value to be compared to the Hash32() value of the execution log trace
  10674. // - TInterfaceStub/TInterfaceMock will check it in their Destroy destructor,
  10675. // using the fLogs[] content
  10676. ExpectedTraceHash: cardinal;
  10677. end;
  10678. /// define the rules for a given method as used internaly by TInterfaceStub
  10679. TInterfaceStubRules = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  10680. /// the mocking / stubing rules associated to this method
  10681. Rules: array of TInterfaceStubRule;
  10682. /// index in Rules[] of the default rule, i.e. the one with Params=''
  10683. DefaultRule: integer;
  10684. /// the number of times this method has been executed
  10685. MethodPassCount: cardinal;
  10686. /// find a rule index from its Params content
  10687. function FindRuleIndex(const aParams: RawUTF8): integer;
  10688. /// find a strong rule index from its Params content
  10689. function FindStrongRuleIndex(const aParams: RawUTF8): integer;
  10690. /// register a rule
  10691. procedure AddRule(Sender: TInterfaceStub; aKind: TInterfaceStubRuleKind;
  10692. const aParams, aValues: RawUTF8; const aEvent: TNotifyEvent=nil;
  10693. aExceptionClass: ExceptClass=nil;
  10694. aExpectedPassCountOperator: TSQLQueryOperator=qoNone; aValue: cardinal=0);
  10695. end;
  10696. /// diverse options available to TInterfaceStub
  10697. // - by default, method execution stack is not recorded - include
  10698. // imoLogMethodCallsAndResults in the options to track all method calls
  10699. // and the returned values; note that ExpectsTrace() method will set it
  10700. // - by default, TInterfaceStub will be released when the stubed/mocked
  10701. // interface is released - include imoFakeInstanceWontReleaseTInterfaceStub
  10702. // in the options to force manual memory handling of TInterfaceStubs
  10703. // - by default, all interfaces will return some default values, unless
  10704. // imoRaiseExceptionIfNoRuleDefined or imoReturnErrorIfNoRuleDefined is
  10705. // included in the options
  10706. // - by default, any TInterfaceMock.Fails() rule execution will notify the
  10707. // TSynTestCase, unless imoMockFailsWillPassTestCase which will let test pass
  10708. TInterfaceStubOption = (
  10709. imoLogMethodCallsAndResults,
  10710. imoFakeInstanceWontReleaseTInterfaceStub,
  10711. imoRaiseExceptionIfNoRuleDefined,
  10712. imoReturnErrorIfNoRuleDefined,
  10713. imoMockFailsWillPassTestCase);
  10714. /// set of options available to TInterfaceStub
  10715. TInterfaceStubOptions = set of TInterfaceStubOption;
  10716. /// every potential part of TInterfaceStubLog.AddAsText() log entry
  10717. TInterfaceStubLogLayout = (wName, wParams, wResults);
  10718. /// set the output layout of TInterfaceStubLog.AddAsText() log entry
  10719. TInterfaceStubLogLayouts = set of TInterfaceStubLogLayout;
  10720. /// used to keep track of one stubbed method call
  10721. TInterfaceStubLog = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  10722. /// call timestamp, in milliseconds
  10723. // - is filled with GetTickCount64() API returned value
  10724. TimeStamp64: Int64;
  10725. /// set to TRUE if this calls failed
  10726. // - i.e. if EInterfaceFactoryException was raised for TInterfaceStub, or
  10727. // if TInterfaceMock did notify its associated TSynTestCase via a Check()
  10728. // - CustomResults/Results will contain the error message
  10729. WasError: boolean;
  10730. /// the method called
  10731. // - a pointer to the existing information in shared TInterfaceFactory
  10732. Method: PServiceMethod;
  10733. /// the parameters at execution call
  10734. Params: RawUTF8;
  10735. /// any non default result returned after execution
  10736. // - if not set (i.e. if equals ''), Method^.DefaultResult has been returned
  10737. // - if WasError is TRUE, always contain the error message
  10738. CustomResults: RawUTF8;
  10739. /// the result returned after execution
  10740. // - this method will return Method^.DefaultResult
  10741. function Results: RawUTF8;
  10742. /// append the log in textual format
  10743. // - typical output is as such:
  10744. // $ Add(10,20)=[30],
  10745. // or, if WasError is TRUE:
  10746. // $ Divide(20,0) error "divide by zero",
  10747. procedure AddAsText(WR: TTextWriter; aScope: TInterfaceStubLogLayouts;
  10748. SepChar: AnsiChar=',');
  10749. end;
  10750. /// used to keep track of all stubbed methods calls
  10751. TInterfaceStubLogDynArray = array of TInterfaceStubLog;
  10752. /// used to stub an interface implementation
  10753. // - define the expected workflow in a fluent interface using Executes /
  10754. // Fails / Returns / Raises
  10755. // - this class will be inherited by TInterfaceMock which will contain some
  10756. // additional methods dedicated to mocking behavior (e.g. including in tests)
  10757. // - each instance of this class will be owned by its generated fake
  10758. // implementation class (retrieved at constructor out parameter): when the
  10759. // stubed/mocked interface is freed, its associated TInterfaceStub will be
  10760. // freed - so you do not need to protect TInterfaceStub.Create with a
  10761. // try..finally clause, since it will be released when no more needed
  10762. // - inherits from TInterfaceResolver so match TInjectableObject expectations
  10763. TInterfaceStub = class(TInterfaceResolver)
  10764. protected
  10765. fInterface: TInterfaceFactory;
  10766. fRules: array of TInterfaceStubRules;
  10767. fOptions: TInterfaceStubOptions;
  10768. fHasExpects: set of (eCount,eTrace);
  10769. fLogs: TInterfaceStubLogDynArray;
  10770. fLog: TDynArray;
  10771. fLogCount: integer;
  10772. fInterfaceExpectedTraceHash: cardinal;
  10773. fLastInterfacedObjectFake: TInterfacedObject;
  10774. function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override;
  10775. function Implements(aInterface: PTypeInfo): boolean; override;
  10776. procedure InternalGetInstance(out aStubbedInterface); virtual;
  10777. function InternalCheck(aValid,aExpectationFailed: boolean;
  10778. const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean; virtual;
  10779. // match TOnFakeInstanceInvoke callback signature
  10780. function Invoke(const aMethod: TServiceMethod;
  10781. const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8;
  10782. aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean;
  10783. // will launch InternalCheck() process if some expectations defined by
  10784. // ExpectsCount() are not met, i.e. raise an exception for TInterfaceStub
  10785. // or notify the associated test case for TInterfaceMock
  10786. procedure InstanceDestroyed(aClientDrivenID: cardinal);
  10787. procedure IntSetOptions(Options: TInterfaceStubOptions); virtual;
  10788. procedure IntCheckCount(aMethodIndex, aComputed: cardinal; aOperator: TSQLQueryOperator; aCount: cardinal);
  10789. function IntGetLogAsText(asmndx: integer; const aParams: RawUTF8;
  10790. aScope: TInterfaceStubLogLayouts; SepChar: AnsiChar): RawUTF8;
  10791. function GetLogHash: cardinal;
  10792. procedure OnExecuteToLog(Ctxt: TOnInterfaceStubExecuteParamsVariant);
  10793. /// low-level internal constructor
  10794. constructor Create(aFactory: TInterfaceFactory;
  10795. const aInterfaceName: RawUTF8); reintroduce; overload; virtual;
  10796. public
  10797. /// initialize an interface stub from TypeInfo(IMyInterface)
  10798. // - assign the fake class instance to a stubbed interface variable:
  10799. // !var I: ICalculator;
  10800. // ! TInterfaceStub.Create(TypeInfo(ICalculator),I);
  10801. // ! Check(I.Add(10,20)=0,'Default result');
  10802. constructor Create(aInterface: PTypeInfo; out aStubbedInterface); reintroduce; overload;
  10803. /// initialize an interface stub from an interface GUID
  10804. // - you shall have registered the interface by a previous call to
  10805. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...])
  10806. // - once registered, create and use the fake class instance as such:
  10807. // !var I: ICalculator;
  10808. // ! TInterfaceStub.Create(ICalculator,I);
  10809. // ! Check(I.Add(10,20)=0,'Default result');
  10810. // - if the supplied TGUID has not been previously registered, raise an Exception
  10811. constructor Create(const aGUID: TGUID; out aStubbedInterface); reintroduce; overload;
  10812. /// initialize an interface stub from an interface name (e.g. 'IMyInterface')
  10813. // - you shall have registered the interface by a previous call to
  10814. // TInterfaceFactory.Get(TypeInfo(IMyInterface)) or RegisterInterfaces([])
  10815. // - if the supplied name has not been previously registered, raise an Exception
  10816. constructor Create(const aInterfaceName: RawUTF8; out aStubbedInterface); reintroduce; overload;
  10817. /// prepare an interface stub from TypeInfo(IMyInterface) for later injection
  10818. // - create several TInterfaceStub instances for a given TInjectableObject
  10819. // ! procedure TMyTestCase.OneTestCaseMethod;
  10820. // ! var Test: IServiceToBeTested;
  10821. // ! begin
  10822. // ! Test := TServiceToBeTested.CreateInjected([],
  10823. // ! TInterfaceStub.Create(TypeInfo(ICalculator)),
  10824. // ! TInterfaceMock.Create(TypeInfo(IPersistence),self).
  10825. // ! ExpectsCount('SaveItem',qoEqualTo,1)]);
  10826. constructor Create(aInterface: PTypeInfo); reintroduce; overload;
  10827. /// prepare an interface stub from a given TGUID for later injection
  10828. // - you shall have registered the interface by a previous call to
  10829. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...])
  10830. // - then create TInterfaceStub instances for a given TInjectableObject:
  10831. // ! procedure TMyTestCase.OneTestCaseMethod;
  10832. // ! var Test: IServiceToBeTested;
  10833. // ! begin
  10834. // ! Test := TServiceToBeTested.CreateInjected(
  10835. // ! [IMyInterface],
  10836. // ! TInterfaceMock.Create(IPersistence,self).
  10837. // ! ExpectsCount('SaveItem',qoEqualTo,1)]);
  10838. constructor Create(const aGUID: TGUID); reintroduce; overload;
  10839. /// add an execution rule for a given method, with JSON marshalling
  10840. // - optional aEventParams parameter will be transmitted to aEvent handler
  10841. // - raise an Exception if the method name does not exist for this interface
  10842. function Executes(const aMethodName: RawUTF8;
  10843. aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8=''): TInterfaceStub; overload;
  10844. /// add an execution rule for a given method and a set of parameters,
  10845. // with JSON marshalling
  10846. // - if execution context matches the supplied aParams value, aEvent is triggered
  10847. // - optional aEventParams parameter will be transmitted to aEvent handler
  10848. // - raise an Exception if the method name does not exist for this interface
  10849. function Executes(const aMethodName, aParams: RawUTF8;
  10850. aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8=''): TInterfaceStub; overload;
  10851. /// add an execution rule for a given method and a set of parameters,
  10852. // with JSON marshalling
  10853. // - if execution context matches the supplied aParams value, aEvent is triggered
  10854. // - optional aEventParams parameter will be transmitted to aEvent handler
  10855. // - raise an Exception if the method name does not exist for this interface
  10856. function Executes(const aMethodName: RawUTF8; const aParams: array of const;
  10857. aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8=''): TInterfaceStub; overload;
  10858. {$ifndef NOVARIANTS}
  10859. /// add an execution rule for a given method, with Variant marshalling
  10860. // - optional aEventParams parameter will be transmitted to aEvent handler
  10861. // - raise an Exception if the method name does not exist for this interface
  10862. function Executes(const aMethodName: RawUTF8;
  10863. aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload;
  10864. /// add an execution rule for a given method and a set of parameters,
  10865. // with Variant marshalling
  10866. // - if execution context matches the supplied aParams value, aEvent is triggered
  10867. // - optional aEventParams parameter will be transmitted to aEvent handler
  10868. // - raise an Exception if the method name does not exist for this interface
  10869. function Executes(const aMethodName, aParams: RawUTF8;
  10870. aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload;
  10871. /// add an execution rule for a given method and a set of parameters,
  10872. // with Variant marshalling
  10873. // - if execution context matches the supplied aParams value, aEvent is triggered
  10874. // - optional aEventParams parameter will be transmitted to aEvent handler
  10875. // - raise an Exception if the method name does not exist for this interface
  10876. function Executes(const aMethodName: RawUTF8; const aParams: array of const;
  10877. aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload;
  10878. /// add an execution rule for all methods, with Variant marshalling
  10879. // - optional aEventParams parameter will be transmitted to aEvent handler
  10880. // - callback's Ctxt: TOnInterfaceStubExecuteParamsVariant's Method field
  10881. // would identify the executed method
  10882. function Executes(aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload;
  10883. /// will add execution rules for all methods to log the input parameters
  10884. // - aKind would define how the input parameters are serialized in JSON
  10885. function Executes(aLog: TSynLogClass; aLogLevel: TSynLogInfo;
  10886. aKind: TServiceMethodParamsDocVariantKind): TInterfaceStub; overload;
  10887. {$endif}
  10888. /// add an exception rule for a given method
  10889. // - will create and raise the specified exception for this method
  10890. // - raise an Exception if the method name does not exist for this interface
  10891. function Raises(const aMethodName: RawUTF8;
  10892. aException: ExceptClass; const aMessage: string): TInterfaceStub; overload;
  10893. /// add an exception rule for a given method and a set of parameters
  10894. // - will create and raise the specified exception for this method, if the
  10895. // execution context matches the supplied aParams value
  10896. // - raise an Exception if the method name does not exist for this interface
  10897. function Raises(const aMethodName, aParams: RawUTF8;
  10898. aException: ExceptClass; const aMessage: string): TInterfaceStub; overload;
  10899. /// add an exception rule for a given method and a set of parameters
  10900. // - will create and raise the specified exception for this method, if the
  10901. // execution context matches the supplied aParams value
  10902. // - raise an Exception if the method name does not exist for this interface
  10903. function Raises(const aMethodName: RawUTF8; const aParams: array of const;
  10904. aException: ExceptClass; const aMessage: string): TInterfaceStub; overload;
  10905. /// add an evaluation rule for a given method
  10906. // - aExpectedResults JSON array will be returned to the caller
  10907. // - raise an Exception if the method name does not exist for this interface
  10908. function Returns(const aMethodName, aExpectedResults: RawUTF8): TInterfaceStub; overload;
  10909. /// add an evaluation rule for a given method
  10910. // - aExpectedResults will be returned to the caller after conversion to
  10911. // a JSON array
  10912. // - raise an Exception if the method name does not exist for this interface
  10913. function Returns(const aMethodName: RawUTF8; const aExpectedResults: array of const): TInterfaceStub; overload;
  10914. /// add an evaluation rule for a given method and a set of parameters
  10915. // - aExpectedResults JSON array will be returned to the caller
  10916. // - raise an Exception if the method name does not exist for this interface
  10917. function Returns(const aMethodName, aParams, aExpectedResults: RawUTF8): TInterfaceStub; overload;
  10918. /// add an evaluation rule for a given method and a set of parameters
  10919. // - aExpectedResults JSON array will be returned to the caller
  10920. // - raise an Exception if the method name does not exist for this interface
  10921. function Returns(const aMethodName: RawUTF8;
  10922. const aParams, aExpectedResults: array of const): TInterfaceStub; overload;
  10923. /// add an error rule for a given method
  10924. // - an error will be returned to the caller, with aErrorMsg as message
  10925. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10926. // TInterfaceMock will push the failure to the associated test case
  10927. // - raise an Exception if the method name does not exist for this interface
  10928. function Fails(const aMethodName, aErrorMsg: RawUTF8): TInterfaceStub; overload;
  10929. /// add an error rule for a given method and a set of parameters
  10930. // - an error will be returned to the caller, with aErrorMsg as message
  10931. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10932. // TInterfaceMock will push the failure to the associated test case
  10933. // - raise an Exception if the method name does not exist for this interface
  10934. function Fails(const aMethodName, aParams, aErrorMsg: RawUTF8): TInterfaceStub; overload;
  10935. /// add an error rule for a given method and a set of parameters
  10936. // - an error will be returned to the caller, with aErrorMsg as message
  10937. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10938. // TInterfaceMock will push the failure to the associated test case
  10939. // - raise an Exception if the method name does not exist for this interface
  10940. function Fails(const aMethodName: RawUTF8; const aParams: array of const;
  10941. const aErrorMsg: RawUTF8): TInterfaceStub; overload;
  10942. /// add a pass count expectation rule for a given method
  10943. // - those rules will be evaluated at Destroy execution
  10944. // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here
  10945. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10946. // TInterfaceMock will push the failure to the associated test case
  10947. // - raise an Exception if the method name does not exist for this interface
  10948. function ExpectsCount(const aMethodName: RawUTF8; aOperator: TSQLQueryOperator;
  10949. aValue: cardinal): TInterfaceStub; overload;
  10950. /// add a pass count expectation rule for a given method and a set of parameters
  10951. // - those rules will be evaluated at Destroy execution
  10952. // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here
  10953. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10954. // TInterfaceMock will push the failure to the associated test case
  10955. // - raise an Exception if the method name does not exist for this interface
  10956. function ExpectsCount(const aMethodName, aParams: RawUTF8; aOperator: TSQLQueryOperator;
  10957. aValue: cardinal): TInterfaceStub; overload;
  10958. /// add a pass count expectation rule for a given method and a set of parameters
  10959. // - those rules will be evaluated at Destroy execution
  10960. // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here
  10961. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10962. // TInterfaceMock will push the failure to the associated test case
  10963. // - raise an Exception if the method name does not exist for this interface
  10964. function ExpectsCount(const aMethodName: RawUTF8; const aParams: array of const;
  10965. aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub; overload;
  10966. /// add a hash-based execution expectation rule for the whole interface
  10967. // - those rules will be evaluated at Destroy execution
  10968. // - supplied aValue is a Hash32() of the trace in LogAsText format
  10969. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10970. // TInterfaceMock will push the failure to the associated test case
  10971. function ExpectsTrace(aValue: cardinal): TInterfaceStub; overload;
  10972. /// add a hash-based execution expectation rule for a given method
  10973. // - those rules will be evaluated at Destroy execution
  10974. // - supplied aValue is a Hash32() of the trace in LogAsText format
  10975. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10976. // TInterfaceMock will push the failure to the associated test case
  10977. // - raise an Exception if the method name does not exist for this interface
  10978. function ExpectsTrace(const aMethodName: RawUTF8; aValue: cardinal): TInterfaceStub; overload;
  10979. /// add a hash-based execution expectation rule for a given method
  10980. // and a set of parameters
  10981. // - those rules will be evaluated at Destroy execution
  10982. // - supplied aValue is a Hash32() of the trace in LogAsText format
  10983. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10984. // TInterfaceMock will push the failure to the associated test case
  10985. // - raise an Exception if the method name does not exist for this interface
  10986. function ExpectsTrace(const aMethodName, aParams: RawUTF8;
  10987. aValue: cardinal): TInterfaceStub; overload;
  10988. /// add a hash-based execution expectation rule for a given method
  10989. // and a set of parameters
  10990. // - those rules will be evaluated at Destroy execution
  10991. // - supplied aValue is a Hash32() of the trace in LogAsText format
  10992. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  10993. // TInterfaceMock will push the failure to the associated test case
  10994. // - raise an Exception if the method name does not exist for this interface
  10995. function ExpectsTrace(const aMethodName: RawUTF8; const aParams: array of const;
  10996. aValue: cardinal): TInterfaceStub; overload;
  10997. /// add a JSON-based execution expectation rule for the whole interface
  10998. // - those rules will be evaluated at Destroy execution
  10999. // - supplied aValue is the trace in LogAsText format
  11000. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  11001. // TInterfaceMock will push the failure to the associated test case
  11002. function ExpectsTrace(const aValue: RawUTF8): TInterfaceStub; overload;
  11003. /// add a JSON-based execution expectation rule for a given method
  11004. // - those rules will be evaluated at Destroy execution
  11005. // - supplied aValue is the trace in LogAsText format
  11006. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  11007. // TInterfaceMock will push the failure to the associated test case
  11008. // - raise an Exception if the method name does not exist for this interface
  11009. function ExpectsTrace(const aMethodName, aValue: RawUTF8): TInterfaceStub; overload;
  11010. /// add a JSON-based execution expectation rule for a given method
  11011. // and a set of parameters
  11012. // - those rules will be evaluated at Destroy execution
  11013. // - supplied aValue is the trace in LogAsText format
  11014. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  11015. // TInterfaceMock will push the failure to the associated test case
  11016. // - raise an Exception if the method name does not exist for this interface
  11017. function ExpectsTrace(const aMethodName, aParams, aValue: RawUTF8): TInterfaceStub; overload;
  11018. /// add a JSON-based execution expectation rule for a given method
  11019. // and a set of parameters
  11020. // - those rules will be evaluated at Destroy execution
  11021. // - supplied aValue is the trace in LogAsText format
  11022. // - it will raise EInterfaceFactoryException for TInterfaceStub, but
  11023. // TInterfaceMock will push the failure to the associated test case
  11024. // - raise an Exception if the method name does not exist for this interface
  11025. function ExpectsTrace(const aMethodName: RawUTF8; const aParams: array of const;
  11026. const aValue: RawUTF8): TInterfaceStub; overload;
  11027. /// set the optional stubing/mocking options
  11028. // - same as the Options property, but in a fluent-style interface
  11029. function SetOptions(Options: TInterfaceStubOptions): TInterfaceStub;
  11030. /// reset the internal trace
  11031. // - Log, LogAsText, LogHash and LogCount would be initialized
  11032. procedure ClearLog;
  11033. /// the stubbed method execution trace items
  11034. property Log: TInterfaceStubLogDynArray read fLogs;
  11035. /// the stubbed method execution trace converted as text
  11036. // - typical output is a list of calls separated by commas:
  11037. // $ Add(10,20)=[30],Divide(20,0) error "divide by zero"
  11038. function LogAsText(SepChar: AnsiChar=','): RawUTF8;
  11039. /// returns the last created TInterfacedObject instance
  11040. // - e.g. corresponding to the out aStubbedInterface parameter of Create()
  11041. property LastInterfacedObjectFake: TInterfacedObject read fLastInterfacedObjectFake;
  11042. published
  11043. /// access to the registered Interface RTTI information
  11044. property InterfaceFactory: TInterfaceFactory read fInterface;
  11045. /// optional stubing/mocking options
  11046. // - you can use the SetOptions() method in a fluent-style interface
  11047. property Options: TInterfaceStubOptions read fOptions write IntSetOptions;
  11048. /// the stubbed method execution trace number of items
  11049. property LogCount: Integer read fLogCount;
  11050. /// the stubbed method execution trace converted as one numerical hash
  11051. // - returns Hash32(LogAsText)
  11052. property LogHash: cardinal read GetLogHash;
  11053. end;
  11054. /// used to mock an interface implementation via expect-run-verify pattern
  11055. // - TInterfaceStub will raise an exception on Fails(), ExpectsCount() or
  11056. // ExpectsTrace() rule activation, but TInterfaceMock will call
  11057. // TSynTestCase.Check() with no exception with such rules, as expected by
  11058. // a mocked interface
  11059. // - this class will follow the expect-run-verify pattern, i.e. expectations
  11060. // are defined before running the test, and verification is performed
  11061. // when the instance is released - use TInterfaceMockSpy if you prefer the
  11062. // more explicit run-verify pattern
  11063. TInterfaceMock = class(TInterfaceStub)
  11064. protected
  11065. fTestCase: TSynTestCase;
  11066. function InternalCheck(aValid,aExpectationFailed: boolean;
  11067. const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean; override;
  11068. public
  11069. /// initialize an interface mock from TypeInfo(IMyInterface)
  11070. // - aTestCase.Check() will be called in case of mocking failure
  11071. // ! procedure TMyTestCase.OneTestCaseMethod;
  11072. // ! var Persist: IPersistence;
  11073. // ! ...
  11074. // ! TInterfaceMock.Create(TypeInfo(IPersistence),Persist,self).
  11075. // ! ExpectsCount('SaveItem',qoEqualTo,1)]);
  11076. constructor Create(aInterface: PTypeInfo; out aMockedInterface;
  11077. aTestCase: TSynTestCase); reintroduce; overload;
  11078. /// initialize an interface mock from an interface TGUID
  11079. // - aTestCase.Check() will be called during validation of all Expects*()
  11080. // - you shall have registered the interface by a previous call to
  11081. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IPersistence),...])
  11082. // - once registered, create and use the fake class instance as such:
  11083. // !procedure TMyTestCase.OneTestCaseMethod;
  11084. // !var Persist: IPersistence;
  11085. // ! ...
  11086. // ! TInterfaceMock.Create(IPersistence,Persist,self).
  11087. // ! ExpectsCount('SaveItem',qoEqualTo,1)]);
  11088. // - if the supplied TGUID has not been previously registered, raise an Exception
  11089. constructor Create(const aGUID: TGUID; out aMockedInterface;
  11090. aTestCase: TSynTestCase); reintroduce; overload;
  11091. /// initialize an interface mock from an interface name (e.g. 'IMyInterface')
  11092. // - aTestCase.Check() will be called in case of mocking failure
  11093. // - you shall have registered the interface by a previous call to
  11094. // TInterfaceFactory.Get(TypeInfo(IMyInterface)) or RegisterInterfaces()
  11095. // - if the supplied name has not been previously registered, raise an Exception
  11096. constructor Create(const aInterfaceName: RawUTF8; out aMockedInterface;
  11097. aTestCase: TSynTestCase); reintroduce; overload;
  11098. /// initialize an interface mock from TypeInfo(IMyInterface) for later injection
  11099. // - aTestCase.Check() will be called in case of mocking failure
  11100. constructor Create(aInterface: PTypeInfo; aTestCase: TSynTestCase); reintroduce; overload;
  11101. /// initialize an interface mock from TypeInfo(IMyInterface) for later injection
  11102. // - aTestCase.Check() will be called in case of mocking failure
  11103. constructor Create(const aGUID: TGUID; aTestCase: TSynTestCase); reintroduce; overload;
  11104. /// the associated test case
  11105. property TestCase: TSynTestCase read fTestCase;
  11106. end;
  11107. /// how TInterfaceMockSpy.Verify() shall generate the calls trace
  11108. TInterfaceMockSpyCheck = (chkName, chkNameParams, chkNameParamsResults);
  11109. /// used to mock an interface implementation via run-verify pattern
  11110. // - this class will implement a so called "test-spy" mocking pattern, i.e.
  11111. // no expectation is to be declared at first, but all calls are internally
  11112. // logged (i.e. it force imoLogMethodCallsAndResults option to be defined),
  11113. // and can afterwards been check via Verify() calls
  11114. TInterfaceMockSpy = class(TInterfaceMock)
  11115. protected
  11116. procedure IntSetOptions(Options: TInterfaceStubOptions); override;
  11117. /// this will set and force imoLogMethodCallsAndResults option as needed
  11118. constructor Create(aFactory: TInterfaceFactory;
  11119. const aInterfaceName: RawUTF8); override;
  11120. public
  11121. /// check that a method has been called a specify number of times
  11122. procedure Verify(const aMethodName: RawUTF8;
  11123. aOperator: TSQLQueryOperator=qoGreaterThan; aCount: cardinal=0); overload;
  11124. /// check a method calls count with a set of parameters
  11125. // - parameters shall be defined as a JSON array of values
  11126. procedure Verify(const aMethodName, aParams: RawUTF8;
  11127. aOperator: TSQLQueryOperator=qoGreaterThan; aCount: cardinal=0); overload;
  11128. /// check a method calls count with a set of parameters
  11129. // - parameters shall be defined as a JSON array of values
  11130. procedure Verify(const aMethodName: RawUTF8; const aParams: array of const;
  11131. aOperator: TSQLQueryOperator=qoGreaterThan; aCount: cardinal=0); overload;
  11132. /// check an execution trace for the global interface
  11133. // - text trace format shall follow method calls, e.g.
  11134. // ! Verify('Multiply,Add',chkName);
  11135. // or may include parameters:
  11136. // ! Verify('Multiply(10,30),Add(2,35)',chkNameParams);
  11137. // or include parameters and function results:
  11138. // ! Verify('Multiply(10,30)=[300],Add(2,35)=[37]',chkNameParamsResults);
  11139. procedure Verify(const aTrace: RawUTF8; aScope: TInterfaceMockSpyCheck); overload;
  11140. /// check an execution trace for a specified method
  11141. // - text trace format will follow specified scope, e.g.
  11142. // ! Verify('Add','(10,30),(2,35)',chkNameParams);
  11143. // or include parameters and function results:
  11144. // ! Verify('Add','(10,30)=[300],(2,35)=[37]',chkNameParamsResults);
  11145. // - if aMethodName does not exists or aScope=chkName, will raise an exception
  11146. procedure Verify(const aMethodName, aTrace: RawUTF8; aScope: TInterfaceMockSpyCheck); overload;
  11147. /// check an execution trace for a specified method and parameters
  11148. // - text trace format shall contain only results, e.g.
  11149. // ! Verify('Add','2,35','[37]');
  11150. procedure Verify(const aMethodName, aParams, aTrace: RawUTF8); overload;
  11151. /// check an execution trace for a specified method and parameters
  11152. // - text trace format shall contain only results, e.g.
  11153. // ! Verify('Add',[2,35],'[37]');
  11154. procedure Verify(const aMethodName: RawUTF8; const aParams: array of const;
  11155. const aTrace: RawUTF8); overload;
  11156. end;
  11157. {$M+}
  11158. /// an abstract service provider, as registered in TServiceContainer
  11159. // - each registered interface has its own TServiceFactory instance, available
  11160. // as one TSQLServiceContainer item from TSQLRest.Services property
  11161. // - this will be either implemented by a registered TInterfacedObject on the
  11162. // server, or by a on-the-fly generated fake TInterfacedObject class
  11163. // communicating via JSON on a client
  11164. // - TSQLRestServer will have to register an interface implementation as:
  11165. // ! Server.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared);
  11166. // - TSQLRestClientURI will have to register an interface remote access as:
  11167. // ! Client.ServiceRegister([TypeInfo(ICalculator)],sicShared));
  11168. // note that the implementation (TServiceCalculator) remain on the server side
  11169. // only: the client only needs the ICalculator interface
  11170. // - then TSQLRestServer and TSQLRestClientURI will both have access to the
  11171. // service, via their Services property, e.g. as:
  11172. // !var I: ICalculator;
  11173. // !...
  11174. // ! if Services.Info(ICalculator).Get(I) then
  11175. // ! result := I.Add(10,20);
  11176. // which is in practice to be used with the faster wrapper method:
  11177. // ! if Services.Resolve(ICalculator,I) then
  11178. // ! result := I.Add(10,20);
  11179. TServiceFactory = class
  11180. protected
  11181. fInterface: TInterfaceFactory;
  11182. fInterfaceURI: RawUTF8;
  11183. fInterfaceMangledURI: RawUTF8;
  11184. fInstanceCreation: TServiceInstanceImplementation;
  11185. fRest: TSQLRest;
  11186. fSharedInstance: TInterfacedObject;
  11187. fContract: RawUTF8;
  11188. fContractHash: RawUTF8;
  11189. fContractExpected: RawUTF8;
  11190. // per-method execution rights
  11191. fExecution: array of TServiceFactoryExecution;
  11192. function GetInterfaceTypeInfo: PTypeInfo;
  11193. {$ifdef HASINLINE}inline;{$endif}
  11194. function GetInterfaceIID: TGUID;
  11195. {$ifdef HASINLINE}inline;{$endif}
  11196. public
  11197. /// initialize the service provider parameters
  11198. // - it will check and retrieve all methods of the supplied interface,
  11199. // and prepare all internal structures for its serialized execution
  11200. constructor Create(aRest: TSQLRest; aInterface: PTypeInfo;
  11201. aInstanceCreation: TServiceInstanceImplementation;
  11202. const aContractExpected: RawUTF8);
  11203. /// retrieve an instance of this interface
  11204. // - this virtual method will be overridden to reflect the expected
  11205. // behavior of client or server side
  11206. // - can be used as such to resolve an I: ICalculator interface:
  11207. // ! var I: ICalculator;
  11208. // ! begin
  11209. // ! if fClient.Services.Info(TypeInfo(ICalculator)).Get(I) then
  11210. // ! ... use I
  11211. function Get(out Obj): Boolean; virtual; abstract;
  11212. /// retrieve the published signature of this interface
  11213. // - is always available on TServiceFactoryServer, but TServiceFactoryClient
  11214. // will be able to retrieve it only if TServiceContainerServer.PublishSignature
  11215. // is set to TRUE (which is not the default setting, for security reasons)
  11216. function RetrieveSignature: RawUTF8; virtual; abstract;
  11217. /// the associated RESTful instance
  11218. property Rest: TSQLRest read fRest;
  11219. /// access to the registered Interface RTTI information
  11220. property InterfaceFactory: TInterfaceFactory read fInterface;
  11221. /// the registered Interface low-level Delphi RTTI type
  11222. // - just maps InterfaceFactory.InterfaceTypeInfo
  11223. property InterfaceTypeInfo: PTypeInfo read GetInterfaceTypeInfo;
  11224. /// the registered Interface GUID
  11225. // - just maps InterfaceFactory.InterfaceIID
  11226. property InterfaceIID: TGUID read GetInterfaceIID;
  11227. (*/ the service contract, serialized as a JSON object
  11228. - a "contract" is in fact the used interface signature, i.e. its
  11229. implementation mode (InstanceCreation) and all its methods definitions
  11230. - a possible value for a one-method interface defined as such:
  11231. ! function ICalculator.Add(n1,n2: integer): integer;
  11232. may be returned as the following JSON object:
  11233. $ {"contract":"Calculator","implementation":"shared",
  11234. $ "methods":[{"method":"Add",
  11235. $ "arguments":[{"argument":"Self","direction":"in","type":"self"},
  11236. $ {"argument":"n1","direction":"in","type":"integer"},
  11237. $ {"argument":"n2","direction":"in","type":"integer"},
  11238. $ {"argument":"Result","direction":"out","type":"integer"}
  11239. $ ]}]} *)
  11240. property Contract: RawUTF8 read fContract;
  11241. /// the published service contract, as expected by both client and server
  11242. // - by default, will contain ContractHash property value (for security)
  11243. // - but you can override this value using plain Contract or any custom
  11244. // value (e.g. a custom version number) - in this case, both TServiceFactoryClient
  11245. // and TServiceFactoryServer instances must have a matching ContractExpected
  11246. // - this value is returned by a '_contract_' pseudo-method name, with the URI:
  11247. // $ POST /root/Interface._contract_
  11248. // or (if TSQLRestRoutingJSON_RPC is used):
  11249. // $ POST /root/Interface
  11250. // $ (...)
  11251. // $ {"method":"_contract_","params":[]}
  11252. // (e.g. to be checked in TServiceFactoryClient.Create constructor)
  11253. // - if set to SERVICE_CONTRACT_NONE_EXPECTED (i.e. '*'), the client won't
  11254. // check and ask the server contract for consistency: it may be used e.g.
  11255. // for accessing a plain REST HTTP server which is not based on mORMot,
  11256. // so may not implement POST /root/Interface._contract_
  11257. property ContractExpected: RawUTF8 read fContractExpected write fContractExpected;
  11258. published
  11259. /// the registered Interface URI
  11260. // - in fact this is the Interface name without the initial 'I', e.g.
  11261. // 'Calculator' for ICalculator
  11262. property InterfaceURI: RawUTF8 read fInterfaceURI;
  11263. /// the registered Interface mangled URI
  11264. // - in fact this is encoding the GUID using BinToBase64URI(), e.g.
  11265. // ! ['{c9a646d3-9c61-4cb7-bfcd-ee2522c8f633}'] into '00amyWGct0y_ze4lIsj2Mw'
  11266. // - can be substituted to the clear InterfaceURI name
  11267. property InterfaceMangledURI: RawUTF8 read fInterfaceMangledURI;
  11268. /// how each class instance is to be created
  11269. // - only relevant on the server side; on the client side, this class will
  11270. // be accessed only to retrieve a remote access instance, i.e. sicSingle
  11271. property InstanceCreation: TServiceInstanceImplementation read fInstanceCreation;
  11272. /// a hash of the service contract, serialized as a JSON string
  11273. // - this may be used instead of the JSON signature, to enhance security
  11274. // (i.e. if you do not want to publish the available methods, but want
  11275. // to check for the proper synchronization of both client and server)
  11276. // - a possible value may be: "C351335A7406374C"
  11277. property ContractHash: RawUTF8 read fContractHash;
  11278. end;
  11279. {$M-}
  11280. /// server-side service provider uses this to store one internal instance
  11281. // - used by TServiceFactoryServer in sicClientDriven, sicPerSession,
  11282. // sicPerUser or sicPerGroup mode
  11283. TServiceFactoryServerInstance = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  11284. public
  11285. /// the internal Instance ID, as remotely sent in "id":1
  11286. // - is set to 0 when an entry in the array is free
  11287. InstanceID: PtrUInt;
  11288. /// GetTickCount64() time stamp corresponding to the last access of
  11289. // this instance
  11290. LastAccess64: Int64;
  11291. /// the implementation instance itself
  11292. Instance: TInterfacedObject;
  11293. /// used to release the implementation instance
  11294. // - direct FreeAndNil(Instance) may lead to A/V if self has been assigned
  11295. // to an interface to any sub-method on the server side -> dec(RefCount)
  11296. procedure SafeFreeInstance(Factory: TServiceFactoryServer);
  11297. end;
  11298. /// server-side service provider uses this to store its internal instances
  11299. // - used by TServiceFactoryServer in sicClientDriven, sicPerSession,
  11300. // sicPerUser or sicPerGroup mode
  11301. TServiceFactoryServerInstanceDynArray = array of TServiceFactoryServerInstance;
  11302. /// callback called before any interface-method service execution to allow
  11303. // its execution
  11304. // - see Ctxt.Service, Ctxt.ServiceMethodIndex and Ctxt.ServiceParameters
  11305. // are used to identify the executed method context
  11306. // - Method parameter would help identify easily the corresponding method, and
  11307. // would contain in fact Service.InterfaceFactory.Methods[ServiceMethodIndex]
  11308. // - should return TRUE if the method can be executed
  11309. // - should return FALSE if the method should not be executed, and set the
  11310. // corresponding error to the supplied context e.g.
  11311. // ! Ctxt.Error('Unauthorized method',HTML_NOTALLOWED);
  11312. // - i.e. called by TSQLRestServerURIContext.InternalExecuteSOAByInterface
  11313. TOnServiceCanExecute = function(Ctxt: TSQLRestServerURIContext;
  11314. const Method: TServiceMethod): boolean of object;
  11315. /// a service provider implemented on the server side
  11316. // - each registered interface has its own TServiceFactoryServer instance,
  11317. // available as one TSQLServiceContainerServer item from TSQLRest.Services property
  11318. // - will handle the implementation class instances of a given interface
  11319. // - by default, all methods are allowed to execution: you can call AllowAll,
  11320. // DenyAll, Allow or Deny in order to specify your exact security policy
  11321. TServiceFactoryServer = class(TServiceFactory)
  11322. protected
  11323. fInstances: TServiceFactoryServerInstanceDynArray;
  11324. fInstance: TDynArray;
  11325. fInstancesCount: integer;
  11326. fInstanceCurrentID: TID;
  11327. fInstanceTimeOut: cardinal;
  11328. fInstanceLock: TRTLCriticalSection;
  11329. fStats: TSynMonitorInputOutputObjArray;
  11330. fImplementationClass: TInterfacedClass;
  11331. fImplementationClassKind: (ickBlank,
  11332. ickWithCustomCreate, ickInjectable, ickInjectableRest,
  11333. ickFromInjectedResolver, ickFake);
  11334. fImplementationClassInterfaceEntry: PInterfaceEntry;
  11335. fSharedInterface: IInterface;
  11336. fByPassAuthentication: boolean;
  11337. fResultAsJSONObject: boolean;
  11338. fResultAsJSONObjectWithoutResult: boolean;
  11339. fResultAsXMLObject: boolean;
  11340. fResultAsJSONObjectIfAccept: boolean;
  11341. fResultAsXMLObjectNameSpace: RawUTF8;
  11342. fBackgroundThread: TSynBackgroundThreadMethod;
  11343. fOnMethodExecute: TOnServiceCanExecute;
  11344. fOnExecute: array of TServiceMethodExecuteEvent;
  11345. fLogRestBatch: array of TSQLRestBatchLocked; // store one BATCH per Rest
  11346. /// union of all fExecution[].Options
  11347. fAnyOptions: TServiceMethodOptions;
  11348. procedure SetServiceLogByIndex(const aMethods: TInterfaceFactoryMethodBits;
  11349. aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass);
  11350. procedure SetTimeoutSecInt(value: cardinal);
  11351. function GetTimeoutSec: cardinal;
  11352. function GetStat(const aMethod: RawUTF8): TSynMonitorInputOutput;
  11353. /// get an implementation Inst.Instance for the given Inst.InstanceID
  11354. // - is called by ExecuteMethod() in sicClientDrive mode
  11355. // - returns true for successfull {"method":"_free_".. call (aMethodIndex=-1)
  11356. // - otherwise, fill Inst.Instance with the matching implementation (or nil)
  11357. function InternalInstanceRetrieve(var Inst: TServiceFactoryServerInstance;
  11358. aMethodIndex: integer): boolean;
  11359. /// call a given method of this service provider
  11360. // - here Ctxt.ServiceMethodIndex should be the index in fInterface.Methods[]
  11361. // (i.e. excluding _free_/_contract_/_signature_ pseudo-methods)
  11362. // - Ctxt.ServiceMethodIndex=-1, then it will free/release corresponding aInstanceID
  11363. // (is called e.g. from {"method":"_free_", "params":[], "id":1234} )
  11364. // - Ctxt.ServiceParameters is e.g. '[1,2]' i.e. a true JSON array, which
  11365. // will contain the incoming parameters in the same exact order than the
  11366. // corresponding implemented interface method
  11367. // - Ctxt.ID is an optional number, to be used in case of sicClientDriven
  11368. // kind of Instance creation to identify the corresponding client session
  11369. // - returns 200/HTML_SUCCESS on success, or an HTTP error status, with an
  11370. // optional error message in aErrorMsg
  11371. // - on success, Ctxt.Call.OutBody shall contain a serialized JSON object
  11372. // with one nested result property, which may be a JSON array, containing
  11373. // all "var" or "out" parameters values, and then the method main result -
  11374. // for instance, ExecuteMethod(..,'[1,2]') over ICalculator.Add will return:
  11375. // $ {"result":[3],"id":0}
  11376. // the returned "id" number is the Instance identifier to be used for any later
  11377. // sicClientDriven remote call - or just 0 in case of sicSingle or sicShared
  11378. procedure ExecuteMethod(Ctxt: TSQLRestServerURIContext);
  11379. /// called by ExecuteMethod to append input/output params to Sender.TempTextWriter
  11380. procedure OnLogRestExecuteMethod(Sender: TServiceMethodExecute;
  11381. Step: TServiceMethodExecuteEventStep);
  11382. /// this method will create an implementation instance
  11383. // - reference count will be set to one, in order to allow safe passing
  11384. // of the instance into an interface, if AndIncreaseRefCount is TRUE
  11385. // - will handle TInterfacedObjectWithCustomCreate and TInjectableObject
  11386. // as expected, if necessary
  11387. function CreateInstance(AndIncreaseRefCount: boolean): TInterfacedObject;
  11388. public
  11389. /// initialize the service provider on the server side
  11390. // - expect an direct server-side implementation class, which may inherit
  11391. // from plain TInterfacedClass, TInterfacedObjectWithCustomCreate if you
  11392. // need an overridden constructor, or TInjectableObject to support DI/IoC
  11393. // - for sicClientDriven, sicPerSession, sicPerUser or sicPerGroup modes,
  11394. // a time out (in seconds) can be defined (default is 30 minutes) - if the
  11395. // specified aTimeOutSec is 0, interface will be forced in sicSingle mode
  11396. // - you should usualy have to call the TSQLRestServer.ServiceRegister()
  11397. // method instead of calling this constructor directly
  11398. constructor Create(aRestServer: TSQLRestServer; aInterface: PTypeInfo;
  11399. aInstanceCreation: TServiceInstanceImplementation;
  11400. aImplementationClass: TInterfacedClass; const aContractExpected: RawUTF8='';
  11401. aTimeOutSec: cardinal=30*60; aSharedInstance: TInterfacedObject=nil); reintroduce;
  11402. /// release all used memory
  11403. // - e.g. any internal TServiceFactoryServerInstance instances (any shared
  11404. // instance, and all still living instances in sicClientDrive mode)
  11405. destructor Destroy; override;
  11406. /// allow all methods execution for all TSQLAuthGroup
  11407. // - all Groups will be affected by this method (on both client and server sides)
  11408. // - this method returns self in order to allow direct chaining of security
  11409. // calls, in a fluent interface
  11410. function AllowAll: TServiceFactoryServer;
  11411. /// allow all methods execution for the specified TSQLAuthGroup ID(s)
  11412. // - the specified group ID(s) will be used to authorize remote service
  11413. // calls from the client side
  11414. // - you can retrieve a TSQLAuthGroup ID from its identifier, as such:
  11415. // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User');
  11416. // - this method returns self in order to allow direct chaining of security
  11417. // calls, in a fluent interface
  11418. function AllowAllByID(const aGroupID: array of TID): TServiceFactoryServer;
  11419. /// allow all methods execution for the specified TSQLAuthGroup names
  11420. // - is just a wrapper around the other AllowAllByID() method, retrieving the
  11421. // Group ID from its main field
  11422. // - this method returns self in order to allow direct chaining of security
  11423. // calls, in a fluent interface
  11424. function AllowAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer;
  11425. /// deny all methods execution for all TSQLAuthGroup
  11426. // - all Groups will be affected by this method (on both client and server sides)
  11427. // - this method returns self in order to allow direct chaining of security
  11428. // calls, in a fluent interface
  11429. function DenyAll: TServiceFactoryServer;
  11430. /// deny all methods execution for the specified TSQLAuthGroup ID(s)
  11431. // - the specified group ID(s) will be used to authorize remote service
  11432. // calls from the client side
  11433. // - you can retrieve a TSQLAuthGroup ID from its identifier, as such:
  11434. // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User');
  11435. // - this method returns self in order to allow direct chaining of security
  11436. // calls, in a fluent interface
  11437. function DenyAllByID(const aGroupID: array of TID): TServiceFactoryServer;
  11438. /// dent all methods execution for the specified TSQLAuthGroup names
  11439. // - is just a wrapper around the other DenyAllByID() method, retrieving the
  11440. // Group ID from its main field
  11441. // - this method returns self in order to allow direct chaining of security
  11442. // calls, in a fluent interface
  11443. function DenyAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer;
  11444. /// allow specific methods execution for the all TSQLAuthGroup
  11445. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11446. // - all Groups will be affected by this method (on both client and server sides)
  11447. // - this method returns self in order to allow direct chaining of security
  11448. // calls, in a fluent interface
  11449. function Allow(const aMethod: array of RawUTF8): TServiceFactoryServer;
  11450. /// allow specific methods execution for the specified TSQLAuthGroup ID(s)
  11451. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11452. // - the specified group ID(s) will be used to authorize remote service
  11453. // calls from the client side
  11454. // - you can retrieve a TSQLAuthGroup ID from its identifier, as such:
  11455. // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User');
  11456. // - this method returns self in order to allow direct chaining of security
  11457. // calls, in a fluent interface
  11458. function AllowByID(const aMethod: array of RawUTF8; const aGroupID: array of TID): TServiceFactoryServer;
  11459. /// allow specific methods execution for the specified TSQLAuthGroup name(s)
  11460. // - is just a wrapper around the other AllowByID() method, retrieving the
  11461. // Group ID from its main field
  11462. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11463. // - this method returns self in order to allow direct chaining of security
  11464. // calls, in a fluent interface
  11465. function AllowByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer;
  11466. /// deny specific methods execution for the all TSQLAuthGroup
  11467. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11468. // - all Groups will be affected by this method (on both client and server sides)
  11469. // - this method returns self in order to allow direct chaining of security
  11470. // calls, in a fluent interface
  11471. function Deny(const aMethod: array of RawUTF8): TServiceFactoryServer;
  11472. /// deny specific methods execution for the specified TSQLAuthGroup ID(s)
  11473. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11474. // - the specified group ID(s) will be used to unauthorize remote service
  11475. // calls from the client side
  11476. // - you can retrieve a TSQLAuthGroup ID from its identifier, as such:
  11477. // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User');
  11478. // - this method returns self in order to allow direct chaining of security
  11479. // calls, in a fluent interface
  11480. function DenyByID(const aMethod: array of RawUTF8; const aGroupID: array of TID): TServiceFactoryServer; overload;
  11481. /// deny specific methods execution for the specified TSQLAuthGroup name(s)
  11482. // - is just a wrapper around the other DenyByID() method, retrieving the
  11483. // Group ID from its main field
  11484. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11485. // - this method returns self in order to allow direct chaining of security
  11486. // calls, in a fluent interface
  11487. function DenyByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer;
  11488. /// define execution options for a given set of methods
  11489. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11490. // - if no method name is given (i.e. []), option will be set for all methods
  11491. // - include optExecInMainThread will force the method(s) to be called within
  11492. // a RunningThread.Synchronize() call - slower, but thread-safe
  11493. // - this method returns self in order to allow direct chaining of security
  11494. // calls, in a fluent interface
  11495. function SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions): TServiceFactoryServer;
  11496. /// define the the instance life time-out, in seconds
  11497. // - for sicClientDriven, sicPerSession, sicPerUser or sicPerGroup modes
  11498. // - raise an exception for other kind of execution
  11499. // - this method returns self in order to allow direct chaining of setting
  11500. // calls for the service, in a fluent interface
  11501. function SetTimeoutSec(value: cardinal): TServiceFactoryServer;
  11502. /// log method execution information to a TSQLRecordServiceLog table
  11503. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11504. // - if no method name is given (i.e. []), option will be set for all methods
  11505. // - will write to the specified aLogRest instance, and would disable
  11506. // writing if aLogRest is nil
  11507. // - will write to a (inherited) TSQLRecordServiceLog table, as available in
  11508. // TSQLRest's model, unless a dedicated table is specified as aLogClass
  11509. // - this method returns self in order to allow direct chaining of security
  11510. // calls, in a fluent interface
  11511. function SetServiceLog(const aMethod: array of RawUTF8;
  11512. aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass=nil): TServiceFactoryServer;
  11513. /// you can define here an event to allow/deny execution of any method
  11514. // of this service, at runtime
  11515. property OnMethodExecute: TOnServiceCanExecute read fOnMethodExecute write fOnMethodExecute;
  11516. /// allow to hook the methods execution
  11517. // - several events could be registered, and would be called directly
  11518. // before and after method execution
  11519. // - if optInterceptInputOutput is defined in Options, then Sender.Input/Output
  11520. // fields would contain the execution data context when Hook is called
  11521. // - see OnMethodExecute if you want to implement security features
  11522. procedure AddInterceptor(const Hook: TServiceMethodExecuteEvent);
  11523. /// retrieve an instance of this interface from the server side
  11524. // - sicShared mode will retrieve the shared instance
  11525. // - sicPerThread mode will retrieve the instance corresponding to the
  11526. // current running thread
  11527. // - all other kind of instance creation will behave the same as sicSingle
  11528. // when accessed directly from this method, i.e. from server side: in fact,
  11529. // on the server side, there is no notion of client, session, user nor group
  11530. // - if ServiceContext.Factory is nil (i.e. if there is no other
  11531. // service context currently associated), this method will also update
  11532. // ServiceContext.Factory, so that the implementation method would be able
  11533. // to access the associated TSQLRestServer instance if needed
  11534. function Get(out Obj): Boolean; override;
  11535. /// retrieve the published signature of this interface
  11536. // - is always available on TServiceFactoryServer, but TServiceFactoryClient
  11537. // will be able to retrieve it only if TServiceContainerServer.PublishSignature
  11538. // is set to TRUE (which is not the default setting, for security reasons)
  11539. function RetrieveSignature: RawUTF8; override;
  11540. /// just type-cast the associated TSQLRest instance to a true TSQLRestServer
  11541. function RestServer: TSQLRestServer;
  11542. {$ifdef HASINLINE}inline;{$endif}
  11543. /// direct access to per-method detailed process statistics
  11544. // - this Stats[] array follows Interface.Methods[] order
  11545. // - see Stat[] property to retrieve information about a method by name
  11546. property Stats: TSynMonitorInputOutputObjArray read fStats;
  11547. /// retrieve detailed statistics about a method use
  11548. // - will return a reference to the actual item in Stats[]: caller should
  11549. // not free the returned instance
  11550. property Stat[const aMethod: RawUTF8]: TSynMonitorInputOutput read GetStat;
  11551. published
  11552. /// the class type used to implement this interface
  11553. property ImplementationClass: TInterfacedClass read fImplementationClass;
  11554. /// the instance life time-out, in seconds
  11555. // - for sicClientDriven, sicPerSession, sicPerUser or sicPerGroup modes
  11556. // - raise an exception for other kind of execution
  11557. // - you can also use the SetTimeOutSec() fluent function instead
  11558. property TimeoutSec: cardinal read GetTimeoutSec write SetTimeoutSecInt;
  11559. /// set to TRUE disable Authentication method check for the whole interface
  11560. // - by default (FALSE), all interface-based services will require valid
  11561. // RESTful authentication (if enabled on the server side); setting TRUE will
  11562. // disable authentication for all methods of this interface
  11563. // (e.g. for returning some HTML content from a public URI, or to implement
  11564. // a public service catalog)
  11565. property ByPassAuthentication: boolean read fByPassAuthentication write fByPassAuthentication;
  11566. /// set to TRUE to return the interface's methods result as JSON object
  11567. // - by default (FALSE), any method execution will return a JSON array with
  11568. // all VAR/OUT parameters, in order
  11569. // - TRUE will generate a JSON object instead, with the VAR/OUT parameter
  11570. // names as field names (and "Result" for any function result) - may be
  11571. // useful e.g. when working with JavaScript clients
  11572. // - Delphi clients (i.e. TServiceFactoryClient/TInterfacedObjectFake) will
  11573. // transparently handle both formats
  11574. // - this value can be overridden by setting ForceServiceResultAsJSONObject
  11575. // for a given TSQLRestServerURIContext (e.g. for server-side JavaScript work)
  11576. property ResultAsJSONObject: boolean
  11577. read fResultAsJSONObject write fResultAsJSONObject;
  11578. /// set to TRUE to return the interface's methods result as JSON object
  11579. // with no '{"result":{...}}' nesting
  11580. // - could be used e.g. for plain non mORMot REST Client with in sicSingle
  11581. // or sicShared mode kind of services
  11582. property ResultAsJSONObjectWithoutResult: boolean
  11583. read fResultAsJSONObjectWithoutResult write fResultAsJSONObjectWithoutResult;
  11584. /// set to TRUE to return the interface's methods result as XML object
  11585. // - by default (FALSE), method execution will return a JSON array with
  11586. // all VAR/OUT parameters, or a JSON object if ResultAsJSONObject is TRUE
  11587. // - TRUE will generate a XML object instead, with the VAR/OUT parameter
  11588. // names as field names (and "Result" for any function result) - may be
  11589. // useful e.g. when working with some XML-only clients
  11590. // - Delphi clients (i.e. TServiceFactoryClient/TInterfacedObjectFake) does
  11591. // NOT handle this XML format yet
  11592. // - this value can be overridden by setting ForceServiceResultAsXMLObject
  11593. // for a given TSQLRestServerURIContext instance
  11594. property ResultAsXMLObject: boolean
  11595. read fResultAsXMLObject write fResultAsXMLObject;
  11596. /// set to TRUE to return XML objects for the interface's methods result
  11597. // if the Accept: HTTP header is exactly 'application/xml' or 'text/xml'
  11598. // - the header should be exactly 'Accept: application/xml' or
  11599. // 'Accept: text/xml' (and no other value)
  11600. // - in this case, ForceServiceResultAsXMLObject will be set for this
  11601. // particular TSQLRestServerURIContext instance, and result returned as XML
  11602. // - using this method allows to mix standard JSON requests (from JSON
  11603. // or AJAX clients) and XML requests (from XML-only clients)
  11604. property ResultAsXMLObjectIfAcceptOnlyXML: boolean
  11605. read fResultAsJSONObjectIfAccept write fResultAsJSONObjectIfAccept;
  11606. /// specify a custom name space content when returning a XML object
  11607. // - by default, no name space would be appended - but such rough XML would
  11608. // have potential validation problems
  11609. // - you may use e.g. XMLUTF8_NAMESPACE, which will append <content ...> ...
  11610. // </content> around the generated XML data
  11611. property ResultAsXMLObjectNameSpace: RawUTF8
  11612. read fResultAsXMLObjectNameSpace write fResultAsXMLObjectNameSpace;
  11613. end;
  11614. /// a service provider implemented on the client side
  11615. // - each registered interface has its own TServiceFactoryClient instance,
  11616. // available as one TSQLServiceContainerClient item from TSQLRest.Services property
  11617. // - will emulate "fake" implementation class instance of a given interface
  11618. // and call remotely the server to process the actual implementation
  11619. TServiceFactoryClient = class(TServiceFactory)
  11620. protected
  11621. fForcedURI: RawUTF8;
  11622. fClient: TSQLRestClientURI;
  11623. fParamsAsJSONObject: boolean;
  11624. fResultAsJSONObject: boolean;
  11625. fSendNotificationsThread: TThread;
  11626. fSendNotificationsRest: TSQLRest;
  11627. fSendNotificationsLogClass: TSQLRecordServiceNotificationsClass;
  11628. function CreateFakeInstance: TInterfacedObject;
  11629. function InternalInvoke(const aMethod: RawUTF8; const aParams: RawUTF8='';
  11630. aResult: PRawUTF8=nil; aErrorMsg: PRawUTF8=nil; aClientDrivenID: PCardinal=nil;
  11631. aServiceCustomAnswer: PServiceCustomAnswer=nil; aClient: TSQLRestClientURI=nil): boolean; virtual;
  11632. // match TOnFakeInstanceInvoke callback signature
  11633. function Invoke(const aMethod: TServiceMethod; const aParams: RawUTF8;
  11634. aResult: PRawUTF8; aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal;
  11635. aServiceCustomAnswer: PServiceCustomAnswer): boolean;
  11636. procedure NotifyInstanceDestroyed(aClientDrivenID: cardinal); virtual;
  11637. public
  11638. /// initialize the service provider parameters
  11639. // - it will check and retrieve all methods of the supplied interface,
  11640. // and prepare all internal structures for its serialized execution
  11641. // - also set the inherited TServiceInstanceImplementation property
  11642. // - initialize fSharedInstance if aInstanceCreation is sicShared
  11643. // - it will also ensure that the corresponding TServiceFactory.Contract
  11644. // matches on both client and server sides, either by comparing the default
  11645. // signature (based on methods and arguments), either by using the supplied
  11646. // expected contract (which may be a custom version number)
  11647. constructor Create(aRest: TSQLRest; aInterface: PTypeInfo;
  11648. aInstanceCreation: TServiceInstanceImplementation;
  11649. const aContractExpected: RawUTF8='');
  11650. /// finalize the service provider used instance
  11651. // - e.g. the shared fake implementation instance
  11652. destructor Destroy; override;
  11653. /// retrieve an instance of this interface from the client side
  11654. function Get(out Obj): Boolean; override;
  11655. /// retrieve the published signature of this interface
  11656. // - TServiceFactoryClient will be able to retrieve it only if
  11657. // TServiceContainerServer.PublishSignature is set to TRUE (which is not the
  11658. // default setting, for security reasons) - this function is always available
  11659. // on TServiceFactoryServer side
  11660. function RetrieveSignature: RawUTF8; override;
  11661. /// convert a HTTP error from mORMot's REST/SOA into an English text message
  11662. // - would recognize the HTML_UNAVAILABLE, HTML_NOTIMPLEMENTED,
  11663. // HTML_NOTALLOWED, HTML_UNAUTHORIZED or HTML_NOTACCEPTABLE errors, as
  11664. // generated by the TSQLRestServer side
  11665. // - is used by TServiceFactoryClient.InternalInvoke, but may be called
  11666. // on client side for TServiceCustomAnswer.Status <> HTML_SUCCESS
  11667. class function GetErrorMessage(status: integer): RawUTF8;
  11668. /// define execution options for a given set of methods
  11669. // - methods names should be specified as an array (e.g. ['Add','Multiply'])
  11670. // - if no method name is given (i.e. []), option will be set for all methods
  11671. // - only supports optNoLogInput and optNoLogOutput on the client side
  11672. procedure SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions);
  11673. /// persist all service calls into a database instead of calling the client
  11674. // - expect a REST instance, which would store all methods without any
  11675. // results (i.e. procedure without any var/out parameters) on the
  11676. // associated TSQLRecordServiceNotifications class
  11677. // - once set, regular fClient.URI() won't be called but a new aLogClass
  11678. // entry would be stored in aRest
  11679. // - to disable this redirection, set aRest and aLogClass to nil
  11680. procedure StoreNotifications(aRest: TSQLRest;
  11681. aLogClass: TSQLRecordServiceNotificationsClass);
  11682. /// allow background process of method with no results, via a temporary
  11683. // database, to be used e.g. for safe notifications transmission
  11684. // - would call StoreNotifications() and start background notification
  11685. // - expect a REST instance, which would store all methods without any
  11686. // results (i.e. procedure without any var/out parameters) on the
  11687. // associated TSQLRecordServiceNotifications class
  11688. // - a background thread would be used to check for pending notifications,
  11689. // and send them to the supplied aRemote TSQLRestClient instance, or
  11690. // to the main TServiceFactoryClient.fClient instance
  11691. // - if the remote client is not reachable, will retry after the specified
  11692. // period of time, in seconds
  11693. // - this method is not blocking, and would write the pending calls to
  11694. // the aRest/aLogClass table, which would be retrieved asynchronously
  11695. // by the background thread
  11696. procedure SendNotifications(aRest: TSQLRest;
  11697. aLogClass: TSQLRecordServiceNotificationsClass; aRetryPeriodSeconds: Integer=30;
  11698. aRemote: TSQLRestClientURI=nil);
  11699. /// compute how many pending notifications are waiting for background process
  11700. // initiated by SendNotifications() method
  11701. function SendNotificationsPending: integer;
  11702. /// wait for all pending notifications to be sent
  11703. // - you can supply a time out period after which no wait would take place
  11704. procedure SendNotificationsWait(aTimeOutSeconds: integer);
  11705. published
  11706. /// could be used to force the remote URI to access the service
  11707. // - by default, the URI would be Root/Calculator or Root/InterfaceMangledURI
  11708. // but you may use this property to use another value, e.g. if you are
  11709. // accessign a non mORMot REST server (probably with aContractExpected set
  11710. // to SERVICE_CONTRACT_NONE_EXPECTED, and running
  11711. // Client.ServerTimeStamp := TimeLogNowUTC to avoid an unsupported
  11712. // ServerTimeStampSynchronize call)
  11713. property ForcedURI: RawUTF8 read fForcedURI write fForcedURI;
  11714. /// set to TRUE to send the interface's methods parameters as JSON object
  11715. // - by default (FALSE), any method execution will send a JSON array with
  11716. // all CONST/VAR parameters, in order
  11717. // - TRUE will generate a JSON object instead, with the CONST/VAR parameter
  11718. // names as field names - may be useful e.g. when working with a non
  11719. // mORMot server
  11720. property ParamsAsJSONObject: boolean read fParamsAsJSONObject write fParamsAsJSONObject;
  11721. /// set to TRUE to expect the interface's methods result to be a JSON object
  11722. // without the {"result":... } nesting
  11723. // - by default (FALSE), any method execution will return a JSON array with
  11724. // all VAR/OUT parameters, within a {"result":...,id:...} layout
  11725. // - TRUE will expect a simple JSON object instead, with the VAR/OUT parameter
  11726. // names as field names (and "Result" for any function result) - may be
  11727. // useful e.g. when working with JavaScript clients
  11728. // - this value can be overridden by setting ForceServiceResultAsJSONObject
  11729. // for a given TSQLRestServerURIContext (e.g. for server-side JavaScript work)
  11730. property ResultAsJSONObjectWithoutResult: boolean read fResultAsJSONObject
  11731. write fResultAsJSONObject;
  11732. end;
  11733. /// used to lookup one method in a global list of interface-based services
  11734. TServiceContainerInterfaceMethod = record
  11735. /// one 'service.method' item, as set at URI
  11736. // - e.g.'Calculator.Add','Calculator.Multiply'...
  11737. InterfaceDotMethodName: RawUTF8;
  11738. /// the associated service provider
  11739. InterfaceService: TServiceFactory;
  11740. /// the index of the method for the given service
  11741. // - 0..2 indicates _free_/_contract_/_signature_ pseudo-methods
  11742. // - then points to InterfaceService.Interface.Methods[InterfaceMethodIndex-3]
  11743. InterfaceMethodIndex: integer;
  11744. end;
  11745. /// pointer to one method lookup in a global list of interface-based services
  11746. PServiceContainerInterfaceMethod = ^TServiceContainerInterfaceMethod;
  11747. /// used to store all methods in a global list of interface-based services
  11748. TServiceContainerInterfaceMethods = array of TServiceContainerInterfaceMethod;
  11749. /// used in TServiceContainer to identify fListInterfaceMethod[] entries
  11750. TServiceContainerInterfaceMethodBits = set of 0..255;
  11751. /// a global services provider class
  11752. // - used to maintain a list of interfaces implementation
  11753. // - inherits from TInterfaceResolverInjected and its Resolve() methods,
  11754. // compatible with TInjectableObject
  11755. TServiceContainer = class(TInterfaceResolverInjected)
  11756. protected
  11757. fRest: TSQLRest;
  11758. // list of service names ['Calculator',...]
  11759. // - Objects[] = TServiceFactory instance
  11760. fList: TRawUTF8ListHashed;
  11761. // list of service.method ['Calculator.Add','Calculator.Multiply',...]
  11762. fListInterfaceMethod: TServiceContainerInterfaceMethods;
  11763. fListInterfaceMethods: TDynArrayHashed;
  11764. fExpectMangledURI: boolean;
  11765. procedure SetExpectMangledURI(aValue: Boolean);
  11766. procedure SetInterfaceMethodBits(MethodNamesCSV: PUTF8Char;
  11767. IncludePseudoMethods: boolean; out bits: TServiceContainerInterfaceMethodBits);
  11768. function GetMethodName(ListInterfaceMethodIndex: integer): RawUTF8;
  11769. procedure CheckInterface(const aInterfaces: array of PTypeInfo);
  11770. function AddServiceInternal(aService: TServiceFactory): integer;
  11771. function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override;
  11772. /// retrieve a service provider from its URI
  11773. function GetService(const aURI: RawUTF8): TServiceFactory;
  11774. public
  11775. /// initialize the list
  11776. constructor Create(aRest: TSQLRest);
  11777. /// release all registered services
  11778. destructor Destroy; override;
  11779. /// release all services of a TSQLRest instance before shutdown
  11780. // - would allow to properly release any pending callbacks
  11781. // - TSQLRest.Services.Release would call FreeAndNil(fServices)
  11782. procedure Release;
  11783. /// return the number of registered service interfaces
  11784. function Count: integer;
  11785. /// method called on the client side to register a service via its interface(s)
  11786. // - will add a TServiceFactoryClient instance to the internal list
  11787. // - is called e.g. by TSQLRestClientURI.ServiceRegister or even by
  11788. // TSQLRestServer.ServiceRegister(aClient: TSQLRest...) for a remote access -
  11789. // use TServiceContainerServer.AddImplementation() instead for normal
  11790. // server side implementation
  11791. // - will raise an exception on error
  11792. // - will return true if some interfaces have been added
  11793. // - will check for the availability of the interfaces on the server side,
  11794. // with an optional custom contract to be used instead of methods signature
  11795. // (only for the first interface)
  11796. function AddInterface(const aInterfaces: array of PTypeInfo;
  11797. aInstanceCreation: TServiceInstanceImplementation;
  11798. aContractExpected: RawUTF8=''): boolean; overload;
  11799. /// method called on the client side to register a service via one interface
  11800. // - overloaded method returning the corresponding service factory client,
  11801. // or nil on error
  11802. function AddInterface(aInterface: PTypeInfo;
  11803. aInstanceCreation: TServiceInstanceImplementation;
  11804. const aContractExpected: RawUTF8=''): TServiceFactoryClient; overload;
  11805. /// retrieve a service provider from its index in the list
  11806. // - returns nil if out of range index
  11807. function Index(aIndex: integer): TServiceFactory; overload;
  11808. {$ifdef HASINLINE}inline;{$endif}
  11809. /// retrieve a service provider from its GUID / Interface type
  11810. // - you shall have registered the interface by a previous call to
  11811. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...])
  11812. // - on match, it will return the service the corresponding interface factory
  11813. // - returns nil if the GUID does not match any registered interface
  11814. // - can be used as such to resolve an I: ICalculator interface
  11815. // ! if fClient.Services.Info(ICalculator).Get(I) then
  11816. // ! ... use I
  11817. function Info(const aGUID: TGUID): TServiceFactory; overload;
  11818. /// retrieve a service provider from its type information
  11819. // - on match, it will return the service the corresponding interface factory
  11820. // - returns nil if the type information does not match any registered interface
  11821. // - can be used as such to resolve an I: ICalculator interface
  11822. // ! if fClient.Services.Info(TypeInfo(ICalculator)).Get(I) then
  11823. // ! ... use I
  11824. // - is defined as virtual so that e.g. TServiceContainerClient would
  11825. // automatically register the interface, if it was not already done
  11826. function Info(aTypeInfo: PTypeInfo): TServiceFactory; overload; virtual;
  11827. /// notify the other side that the given Callback event interface is released
  11828. // - this default implementation will do nothing
  11829. function CallBackUnRegister(const Callback: IInvokable): boolean; virtual;
  11830. /// retrieve all registered Services TGUID
  11831. procedure SetGUIDs(out Services: TGUIDDynArray);
  11832. /// retrieve all registered Services names
  11833. // - i.e. all interface names without the initial 'I', e.g. 'Calculator' for
  11834. // ICalculator
  11835. procedure SetInterfaceNames(out Names: TRawUTF8DynArray);
  11836. /// retrieve all registered Services contracts as a JSON array
  11837. // - i.e. a JSON array of TServiceFactory.Contract JSON objects
  11838. function AsJson: RawJSON;
  11839. /// retrieve a service provider from its URI
  11840. // - it expects the supplied URI variable to be e.g. '00amyWGct0y_ze4lIsj2Mw'
  11841. // or 'Calculator', depending on the ExpectMangledURI property
  11842. // - on match, it will return the service the corresponding interface factory
  11843. // - returns nil if the URI does not match any registered interface
  11844. property Services[const aURI: RawUTF8]: TServiceFactory read GetService; default;
  11845. /// the associated RESTful instance
  11846. property Rest: TSQLRest read fRest;
  11847. /// set if the URI is expected to be mangled from the GUID
  11848. // - by default (FALSE), the clear service name is expected to be supplied at
  11849. // the URI level (e.g. 'Calculator')
  11850. // - if this property is set to TRUE, the mangled URI value will be expected
  11851. // instead (may enhance security) - e.g. '00amyWGct0y_ze4lIsj2Mw'
  11852. property ExpectMangledURI: boolean read fExpectMangledURI write SetExpectMangledURI;
  11853. end;
  11854. /// a callback interface used to notify a TSQLRecord modification in real time
  11855. // - will be used e.g. by TSQLRestServer.RecordVersionSynchronizeSubscribeMaster()
  11856. // - all methods of this interface will be called asynchronously when
  11857. // transmitted via our WebSockets implementation, since they are defined as
  11858. // plain procedures
  11859. // - each callback instance should be private to a specific TSQLRecord
  11860. IServiceRecordVersionCallback = interface(IInvokable)
  11861. ['{8598E6BE-3590-4F76-9449-7AF7AF4241B0}']
  11862. /// this event will be raised on any Add on a versioned record
  11863. // - the supplied JSON object will contain the TRecordVersion field
  11864. procedure Added(const NewContent: RawJSON);
  11865. /// this event will be raised on any Update on a versioned record
  11866. // - the supplied JSON object will contain the TRecordVersion field
  11867. procedure Updated(const ModifiedContent: RawJSON);
  11868. /// this event will be raised on any Delete on a versioned record
  11869. procedure Deleted(const ID: TID; const Revision: TRecordVersion);
  11870. /// allow to optimize process for WebSockets "jumbo frame" items
  11871. // - this method may be called with isLast=false before the first method
  11872. // call of this interface, then with isLast=true after the call of the
  11873. // last method of the "jumbo frame"
  11874. // - match TInterfaceFactory.MethodIndexCurrentFrameCallback signature
  11875. // - allow e.g. to create a temporary TSQLRestBatch for jumbo frames
  11876. // - if individual frames are received, this method won't be called
  11877. procedure CurrentFrame(isLast: boolean);
  11878. end;
  11879. /// a list of callback interfaces to notify TSQLRecord modifications
  11880. // - you can use InterfaceArray*() wrapper functions to manage the list
  11881. IServiceRecordVersionCallbackDynArray = array of IServiceRecordVersionCallback;
  11882. /// service definition for master/slave replication notifications subscribe
  11883. // - implemented by TServiceRecordVersion, as used by
  11884. // TSQLRestServer.RecordVersionSynchronizeMasterStart(), and expected by
  11885. // TSQLRestServer.RecordVersionSynchronizeSlaveStart()
  11886. IServiceRecordVersion = interface(IInvokable)
  11887. ['{06A355CA-19EB-4CC6-9D87-7B48967D1D9F}']
  11888. /// will register the supplied callback for the given table
  11889. function Subscribe(const SQLTableName: RawUTF8; const revision: TRecordVersion;
  11890. const callback: IServiceRecordVersionCallback): boolean;
  11891. end;
  11892. /// service definition with a method which will be called when a callback
  11893. // interface instance is released on the client side
  11894. // - may be used to implement safe publish/subscribe mechanism using
  11895. // interface callbacks, e.g. over WebSockets
  11896. IServiceWithCallbackReleased = interface(IInvokable)
  11897. ['{8D518FCB-62C3-42EB-9AE7-96ED322140F7}']
  11898. /// will be called when a callback is released on the client side
  11899. // - this method matches the TInterfaceFactory.MethodIndexCallbackReleased
  11900. // signature, so that it would be called with the interface instance by
  11901. // TServiceContainerServer.FakeCallbackRelease
  11902. // - you may use it as such - see sample Project31ChatServer.dpr:
  11903. // ! procedure TChatService.CallbackReleased(const callback: IInvokable;
  11904. // ! const interfaceName: RawUTF8);
  11905. // ! begin // unsubscribe from fConnected: array of IChatCallback
  11906. // ! if interfaceName='IChatCallback' then
  11907. // ! InterfaceArrayDelete(fConnected,callback);
  11908. // ! end;
  11909. procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8);
  11910. end;
  11911. /// event signature triggerred when a callback instance is released
  11912. // - used by TServiceContainerServer.OnCallbackReleasedOnClientSide
  11913. // and TServiceContainerServer.OnCallbackReleasedOnServerSide event properties
  11914. // - the supplied Instance will be a TInterfacedObjectFakeServer, and the
  11915. // Callback would be a pointer to the corresponding interface value
  11916. // - assigned implementation should be as fast a possible, since this event
  11917. // will be executed in a global lock for all server-side callbacks
  11918. TOnCallbackReleased = procedure(Sender: TServiceContainer;
  11919. Instance: TInterfacedObject; Callback: pointer) of object;
  11920. /// how TServiceContainerServer would handle SOA callbacks
  11921. // - by default, a callback released on the client side will log a warning
  11922. // and continue the execution (relying e.g. on a CallbackReleased() method to
  11923. // unsubscribe the event), but coRaiseExceptionIfReleasedByClient can be
  11924. // defined to raise an EInterfaceFactoryException in this case
  11925. TServiceCallbackOptions = set of (
  11926. coRaiseExceptionIfReleasedByClient);
  11927. /// a services provider class to be used on the server side
  11928. // - this will maintain a list of true implementation classes
  11929. TServiceContainerServer = class(TServiceContainer)
  11930. protected
  11931. fPublishSignature: boolean;
  11932. fConnectionID: Int64;
  11933. fFakeCallbacks: TObjectListLocked; // TInterfacedObjectFakeServer instances
  11934. fOnCallbackReleasedOnClientSide: TOnCallbackReleased;
  11935. fOnCallbackReleasedOnServerSide: TOnCallbackReleased;
  11936. fCallbackOptions: TServiceCallbackOptions;
  11937. fRecordVersionCallback: array of IServiceRecordVersionCallbackDynArray;
  11938. /// make some garbage collection when session is finished
  11939. procedure OnCloseSession(aSessionID: cardinal); virtual;
  11940. procedure FakeCallbackAdd(aFakeInstance: TObject);
  11941. procedure FakeCallbackRemove(aFakeInstance: TObject);
  11942. procedure FakeCallbackRelease(Ctxt: TSQLRestServerURIContext);
  11943. procedure RecordVersionCallbackNotify(TableIndex: integer;
  11944. Occasion: TSQLOccasion; const DeletedID: TID;
  11945. const DeletedRevision: TRecordVersion; const AddUpdateJson: RawUTF8);
  11946. public
  11947. /// class method able to check if a given server-side callback event fake
  11948. // instance has been released on the client side
  11949. // - may be used to automatically purge a list of subscribed callbacks,
  11950. // e.g. before trigerring the interface instance, and avoid an exception
  11951. class function CallbackReleasedOnClientSide(const callback: IInterface): boolean;
  11952. /// method called on the server side to register a service via its
  11953. // interface(s) and a specified implementation class or a shared
  11954. // instance (for sicShared mode)
  11955. // - will add a TServiceFactoryServer instance to the internal list
  11956. // - will raise an exception on error
  11957. // - will return the first of the registered TServiceFactoryServer created
  11958. // on success (i.e. the one corresponding to the first item of the aInterfaces
  11959. // array), or nil if registration failed (e.g. if any of the supplied interfaces
  11960. // is not implemented by the given class)
  11961. // - the same implementation class can be used to handle several interfaces
  11962. // (just as Delphi allows to do natively)
  11963. function AddImplementation(aImplementationClass: TInterfacedClass;
  11964. const aInterfaces: array of PTypeInfo;
  11965. aInstanceCreation: TServiceInstanceImplementation;
  11966. aSharedImplementation: TInterfacedObject; const aContractExpected: RawUTF8): TServiceFactoryServer;
  11967. /// finalize the service container
  11968. destructor Destroy; override;
  11969. /// register a callback interface which will be called each time a write
  11970. // operation is performed on a given TSQLRecord with a TRecordVersion field
  11971. // - called e.g. by TSQLRestServer.RecordVersionSynchronizeSubscribeMaster
  11972. function RecordVersionSynchronizeSubscribeMaster(TableIndex: integer;
  11973. RecordVersion: TRecordVersion; const SlaveCallback: IServiceRecordVersionCallback): boolean;
  11974. /// notify any TRecordVersion callback for a table Add/Update from a
  11975. // TDocVariant content
  11976. // - used e.g. by TSQLRestStorageMongoDB.DocFromJSON()
  11977. procedure RecordVersionNotifyAddUpdate(Occasion: TSQLOccasion;
  11978. TableIndex: integer; const Document: TDocVariantData); overload;
  11979. /// notify any TRecordVersion callback for a table Add/Update from a
  11980. // TJSONObjectDecoder content
  11981. // - used e.g. by TSQLRestStorageMongoDB.DocFromJSON()
  11982. procedure RecordVersionNotifyAddUpdate(Occasion: TSQLOccasion;
  11983. TableIndex: integer; const Decoder: TJSONObjectDecoder); overload;
  11984. /// notify any TRecordVersion callback for a table Delete
  11985. procedure RecordVersionNotifyDelete(TableIndex: integer;
  11986. const ID: TID; const Revision: TRecordVersion);
  11987. /// log method execution information to a TSQLRecordServiceLog table
  11988. // - TServiceFactoryServer.SetServiceLog() will be called for all registered
  11989. // interfaced-based services of this container
  11990. // - will write to the specified aLogRest instance, and would disable
  11991. // writing if aLogRest is nil
  11992. // - will write to a (inherited) TSQLRecordServiceLog table, as available in
  11993. // TSQLRest's model, unless a dedicated table is specified as aLogClass
  11994. // - you could specify a CSV list of method names to be excluded from logging
  11995. // (containing e.g. a password or a credit card number), containing either
  11996. // the interface name (as 'ICalculator.Add'), or not (as 'Add')
  11997. procedure SetServiceLog(aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass=nil;
  11998. const aExcludedMethodNamesCSV: RawUTF8='');
  11999. /// defines if the "method":"_signature_" or /root/Interface._signature
  12000. // pseudo method is available to retrieve the whole interface signature,
  12001. // encoded as a JSON object
  12002. // - is set to FALSE by default, for security reasons: only "_contract_"
  12003. // pseudo method is available - see TServiceContainer.ContractExpected
  12004. property PublishSignature: boolean read fPublishSignature write fPublishSignature;
  12005. /// this event will be launched when a callback interface is notified as
  12006. // relased on the Client side
  12007. // - as an alternative, you may define the following method on the
  12008. // registration service interface type, which would be called when a
  12009. // callback registered via this service is released (e.g. to unsubscribe
  12010. // the callback from an interface list, via InterfaceArrayDelete):
  12011. // ! procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8);
  12012. property OnCallbackReleasedOnClientSide: TOnCallbackReleased
  12013. read fOnCallbackReleasedOnClientSide;
  12014. /// this event will be launched when a callback interface is relased on
  12015. // the Server side
  12016. property OnCallbackReleasedOnServerSide: TOnCallbackReleased
  12017. read fOnCallbackReleasedOnServerSide;
  12018. /// defines how SOA callbacks will be handled
  12019. property CallbackOptions: TServiceCallbackOptions read fCallbackOptions
  12020. write fCallbackOptions;
  12021. end;
  12022. /// this class implements a service, which may be called to push notifications
  12023. // for master/slave replication
  12024. // - as used by TSQLRestServer.RecordVersionSynchronizeMasterStart(), and
  12025. // expected by TSQLRestServer.RecordVersionSynchronizeSlaveStart()
  12026. TServiceRecordVersion = class(TInjectableObjectRest,IServiceRecordVersion)
  12027. public
  12028. /// will register the supplied callback for the given table
  12029. function Subscribe(const SQLTableName: RawUTF8; const revision: TRecordVersion;
  12030. const callback: IServiceRecordVersionCallback): boolean;
  12031. end;
  12032. /// a services provider class to be used on the client side
  12033. // - this will maintain a list of fake implementation classes, which will
  12034. // remotely call the server to make the actual process
  12035. TServiceContainerClient = class(TServiceContainer)
  12036. protected
  12037. fDisableAutoRegisterAsClientDriven: boolean;
  12038. public
  12039. /// retrieve a service provider from its type information
  12040. // - this overridden method will register the interface, if was not yet made
  12041. // - in this case, the interface will be registered with sicClientDriven
  12042. // implementation method, unless DisableAutoRegisterAsClientDriven is TRUE
  12043. function Info(aTypeInfo: PTypeInfo): TServiceFactory; overload; override;
  12044. /// notify the other side that the given Callback event interface is released
  12045. // - this overriden implementation will check the private fFakeCallbacks list
  12046. function CallBackUnRegister(const Callback: IInvokable): boolean; override;
  12047. /// allow to disable the automatic registration as sicClientDriven in Info()
  12048. property DisableAutoRegisterAsClientDriven: boolean
  12049. read fDisableAutoRegisterAsClientDriven write fDisableAutoRegisterAsClientDriven;
  12050. end;
  12051. /// TInterfacedObject class which would notify a REST server when it is released
  12052. // - could be used when implementing event callbacks as interfaces, so that
  12053. // the other side instance would be notified when it is destroyed
  12054. TInterfacedCallback = class(TInterfacedObjectLocked)
  12055. protected
  12056. fRest: TSQLRest;
  12057. fInterface: TGUID;
  12058. public
  12059. /// initialize the instance for a given REST and callback interface
  12060. constructor Create(aRest: TSQLRest; const aGUID: TGUID); reintroduce;
  12061. /// notify the associated TSQLRestServer that the callback is disconnnected
  12062. // - i.e. will call TSQLRestServer's TServiceContainer.CallBackUnRegister()
  12063. // - this method will process the unsubscription only once, and
  12064. procedure CallbackRestUnregister; virtual;
  12065. /// finalize the instance, and notify the TSQLRestServer that the callback
  12066. // is now unreachable
  12067. // - i.e. will call CallbackRestUnregister
  12068. destructor Destroy; override;
  12069. /// the associated TSQLRestServer instance, which would be notified
  12070. // when the callback is released
  12071. property Rest: TSQLRest read fRest;
  12072. /// the interface type, implemented by this callback class
  12073. property RestInterface: TGUID read fInterface write fInterface;
  12074. end;
  12075. /// asynchrounous callback to emulate a synchronous/blocking process
  12076. // - once created, process would block via a WaitFor call, which would be
  12077. // released when CallbackFinished() is called by the process background thread
  12078. TBlockingCallback = class(TInterfacedCallback)
  12079. protected
  12080. fProcess: TBlockingProcess;
  12081. function GetEvent: TBlockingEvent;
  12082. public
  12083. /// initialize the callback instance
  12084. // - specify a time out millliseconds period after which blocking execution
  12085. // should be handled as failure (if 0 is set, default 3000 would be used)
  12086. // - you can optionally set a REST and callback interface for automatic
  12087. // notification when this TInterfacedCallback would be released
  12088. constructor Create(aTimeOutMs: integer;
  12089. aRest: TSQLRest; const aGUID: TGUID); reintroduce;
  12090. /// finalize the callback instance
  12091. destructor Destroy; override;
  12092. /// called to wait for the callback to be processed, or trigger timeout
  12093. // - would block until CallbackFinished() is called by the processing thread
  12094. // - returns the final state of the process, i.e. beRaised or beTimeOut
  12095. function WaitFor: TBlockingEvent; virtual;
  12096. /// should be called by the callback when the process is finished
  12097. // - the caller would then let its WaitFor method return
  12098. // - if aServerUnregister is TRUE, will also call CallbackRestUnregister to
  12099. // notify the server that the callback is no longer needed
  12100. // - would optionally log all published properties values to the log class
  12101. // of the supplied REST instance
  12102. procedure CallbackFinished(aRestForLog: TSQLRest;
  12103. aServerUnregister: boolean=false); virtual;
  12104. /// just a wrapper to reset the internal Event state to evNone
  12105. // - may be used to re-use the same TBlockingCallback instance, after
  12106. // a successfull WaitFor/CallbackFinished process
  12107. // - returns TRUE on success (i.e. status was not beWaiting)
  12108. // - if there is a WaitFor currently in progress, returns FALSE
  12109. function Reset: boolean; virtual;
  12110. /// the associated blocking process instance
  12111. property Process: TBlockingProcess read fProcess;
  12112. published
  12113. /// the current state of process
  12114. // - just a wrapper around Process.Event
  12115. // - use Reset method to re-use this instance after a WaitFor process
  12116. property Event: TBlockingEvent read GetEvent;
  12117. end;
  12118. /// this class implements a callback interface, able to write all remote ORM
  12119. // notifications to the local DB
  12120. // - could be supplied as callback parameter, possibly via WebSockets
  12121. // transmission, to TSQLRestServer.RecordVersionSynchronizeSubscribeMaster()
  12122. TServiceRecordVersionCallback = class(TInterfacedCallback,IServiceRecordVersionCallback)
  12123. protected
  12124. fTable: TSQLRecordClass;
  12125. fRecordVersionField: TSQLPropInfoRTTIRecordVersion;
  12126. fBatch: TSQLRestBatch;
  12127. fSlave: TSQLRestServer; // fRest is master remote access
  12128. fOnNotify: TOnBatchWrite;
  12129. // local TSQLRecordTableDeleted.ID follows current Model -> pre-compute offset
  12130. fTableDeletedIDOffset: Int64;
  12131. procedure SetCurrentRevision(const Revision: TRecordVersion; Event: TSQLOccasion);
  12132. public
  12133. /// initialize the instance able to apply callbacks for a given table on
  12134. // a local slave REST server from a remote master REST server
  12135. // - the optional low-level aOnNotify callback will be triggerred for each
  12136. // incoming notification, to track the object changes in real-time
  12137. constructor Create(aSlave: TSQLRestServer; aMaster: TSQLRestClientURI;
  12138. aTable: TSQLRecordClass; aOnNotify: TOnBatchWrite); reintroduce;
  12139. /// finalize this callback instance
  12140. destructor Destroy; override;
  12141. /// this event will be raised on any Add on a versioned record
  12142. procedure Added(const NewContent: RawJSON); virtual;
  12143. /// this event will be raised on any Update on a versioned record
  12144. procedure Updated(const ModifiedContent: RawJSON); virtual;
  12145. /// this event will be raised on any Delete on a versioned record
  12146. procedure Deleted(const ID: TID; const Revision: TRecordVersion); virtual;
  12147. /// match TInterfaceFactory.MethodIndexCurrentFrameCallback signature,
  12148. // so that TSQLHttpClientWebsockets.CallbackRequest will call it
  12149. // - it will create a temporary TSQLRestBatch for the whole "jumbo frame"
  12150. procedure CurrentFrame(isLast: boolean); virtual;
  12151. /// low-level event handler triggerred by Added/Updated/Deleted methods
  12152. property OnNotify: TOnBatchWrite read fOnNotify write fOnNotify;
  12153. end;
  12154. /// for TSQLRestCache, stores a table values
  12155. TSQLRestCacheEntryValue = packed record
  12156. /// corresponding ID
  12157. ID: TID;
  12158. /// JSON encoded UTF-8 serialization of the record
  12159. JSON: RawUTF8;
  12160. /// GetTickCount64() value when this cached value was stored
  12161. // - equals 0 when there is no JSON value cached
  12162. TimeStamp64: Int64;
  12163. end;
  12164. /// for TSQLRestCache, stores all tables values
  12165. TSQLRestCacheEntryValueDynArray = array of TSQLRestCacheEntryValue;
  12166. /// for TSQLRestCache, stores a table settings and values
  12167. TSQLRestCacheEntry = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  12168. public
  12169. /// TRUE if this table should use caching
  12170. // - i.e. if was not set, or worth it for this table (e.g. in-memory table)
  12171. CacheEnable: boolean;
  12172. /// the whole specified Table content will be cached
  12173. CacheAll: boolean;
  12174. /// time out value (in ms)
  12175. // - if 0, caching will never expire
  12176. TimeOutMS: Cardinal;
  12177. /// the number of entries stored in Values[]
  12178. Count: integer;
  12179. /// all cached IDs and JSON content
  12180. Values: TSQLRestCacheEntryValueDynArray;
  12181. /// TDynArray wrapper around the Values[] array
  12182. Value: TDynArray;
  12183. /// used to lock the table cache for multi thread safety
  12184. Mutex: TSynLocker;
  12185. /// initialize this table cache
  12186. // - will set Value wrapper and Mutex handle - other fields should have
  12187. // been cleared by caller (is the case for a TSQLRestCacheEntryDynArray)
  12188. procedure Init;
  12189. /// reset all settings corresponding to this table cache
  12190. procedure Clear;
  12191. /// finalize this table cache entry
  12192. procedure Done;
  12193. /// flush cache for a given Value[] index
  12194. procedure FlushCacheEntry(Index: Integer);
  12195. /// flush cache for all Value[]
  12196. procedure FlushCacheAllEntries;
  12197. /// add the supplied ID to the Value[] array
  12198. procedure SetCache(aID: TID);
  12199. /// update/refresh the cached JSON serialization of a given ID
  12200. procedure SetJSON(aID: TID; const aJSON: RawUTF8); overload;
  12201. /// update/refresh the cached JSON serialization of a supplied Record
  12202. procedure SetJSON(aRecord: TSQLRecord); overload;
  12203. /// retrieve a JSON serialization of a given ID from cache
  12204. function RetrieveJSON(aID: TID; var aJSON: RawUTF8): boolean; overload;
  12205. /// unserialize a JSON cached record of a given ID
  12206. function RetrieveJSON(aID: TID; aValue: TSQLRecord): boolean; overload;
  12207. end;
  12208. /// for TSQLRestCache, stores all table settings and values
  12209. // - this dynamic array will follow TSQLRest.Model.Tables[] layout, i.e. one
  12210. // entry per TSQLRecord class in the data model
  12211. TSQLRestCacheEntryDynArray = array of TSQLRestCacheEntry;
  12212. /// implement a fast TSQLRecord cache, per ID, at the TSQLRest level
  12213. // - purpose of this caching mechanism is to speed up retrieval of some common
  12214. // values at either Client or Server level (like configuration settings)
  12215. // - only caching synchronization is about the following RESTful basic commands:
  12216. // RETRIEVE, ADD, DELETION and UPDATE (that is, a complex direct SQL UPDATE
  12217. // or via TSQLRecordMany pattern won't be taken in account)
  12218. // - only Simple fields are cached: e.g. the BLOB fields are not stored
  12219. // - this cache is thread-safe (access is locked per table)
  12220. // - this caching will be located at the TSQLRest level, that is no automated
  12221. // synchronization is implemented between TSQLRestClient and TSQLRestServer:
  12222. // you shall ensure that your code won't fail due to this restriction
  12223. TSQLRestCache = class
  12224. protected
  12225. fRest: TSQLRest;
  12226. /// fCache[] follows fRest.Model.Tables[] array: one entry per TSQLRecord
  12227. fCache: TSQLRestCacheEntryDynArray;
  12228. /// retrieve a record specified by its ID from cache into JSON content
  12229. // - return '' if the item is not in cache
  12230. function Retrieve(aTableIndex, aID: TID): RawUTF8; overload;
  12231. /// fill a record specified by its ID from cache into a new TSQLRecord instance
  12232. // - return false if the item is not in cache
  12233. // - this method will call RetrieveJSON method, unserializing the cached
  12234. // JSON content into the supplied aValue instance
  12235. function Retrieve(aID: TID; aValue: TSQLRecord): boolean; overload;
  12236. public
  12237. /// create a cache instance
  12238. // - the associated TSQLModel will be used internaly
  12239. constructor Create(aRest: TSQLRest); reintroduce;
  12240. /// release the cache instance
  12241. destructor Destroy; override;
  12242. /// flush the cache
  12243. // - this will flush all stored JSON content, but keep the settings
  12244. // (SetCache/SetTimeOut) as before
  12245. procedure Flush; overload;
  12246. /// flush the cache for a given table
  12247. // - this will flush all stored JSON content, but keep the settings
  12248. // (SetCache/SetTimeOut) as before for this table
  12249. procedure Flush(aTable: TSQLRecordClass); overload;
  12250. /// flush the cache for a given record
  12251. // - this will flush the stored JSON content for this record (and table
  12252. // settings will be kept)
  12253. procedure Flush(aTable: TSQLRecordClass; aID: TID); overload;
  12254. /// flush the cache for a set of specified records
  12255. // - this will flush the stored JSON content for these record (and table
  12256. // settings will be kept)
  12257. procedure Flush(aTable: TSQLRecordClass; const aIDs: array of TID); overload;
  12258. /// flush the cache, and destroy all settings
  12259. // - this will flush all stored JSON content, AND destroy the settings
  12260. // (SetCache/SetTimeOut) to default (i.e. no cache enabled)
  12261. procedure Clear;
  12262. /// activate the internal caching for a whole Table
  12263. // - any cached item of this table will be flushed
  12264. // - return true on success
  12265. function SetCache(aTable: TSQLRecordClass): boolean; overload;
  12266. /// activate the internal caching for a given TSQLRecord
  12267. // - if this item is already cached, do nothing
  12268. // - return true on success
  12269. function SetCache(aTable: TSQLRecordClass; aID: TID): boolean; overload;
  12270. /// activate the internal caching for a set of specified TSQLRecord
  12271. // - if these items are already cached, do nothing
  12272. // - return true on success
  12273. function SetCache(aTable: TSQLRecordClass; const aIDs: array of TID): boolean; overload;
  12274. /// activate the internal caching for a given TSQLRecord
  12275. // - will cache the specified aRecord.ID item
  12276. // - if this item is already cached, do nothing
  12277. // - return true on success
  12278. function SetCache(aRecord: TSQLRecord): boolean; overload;
  12279. /// set the internal caching time out delay (in ms) for a given table
  12280. // - time out setting is common to all items of the table
  12281. // - if aTimeOut is left to its default 0 value, caching will never expire
  12282. // - return true on success
  12283. function SetTimeOut(aTable: TSQLRecordClass; aTimeoutMS: cardinal): boolean;
  12284. /// returns TRUE if the table is part of the current caching policy
  12285. function IsCached(aTable: TSQLRecordClass): boolean;
  12286. /// returns the number of JSON serialization records within this cache
  12287. function CachedEntries: cardinal;
  12288. /// returns the memory used by JSON serialization records within this cache
  12289. // - this method will also flush any outdated entries in the cache
  12290. function CachedMemory(FlushedEntriesCount: PInteger=nil): cardinal;
  12291. /// read-only access to the associated TSQLRest instance
  12292. property Rest: TSQLRest read fRest;
  12293. public { TSQLRest low level methods which are not to be called usualy: }
  12294. /// TSQLRest instance shall call this method when a record is added or updated
  12295. // - this overloaded method expects the content to be specified as JSON object
  12296. procedure Notify(aTable: TSQLRecordClass; aID: TID; const aJSON: RawUTF8;
  12297. aAction: TSQLOccasion); overload;
  12298. /// TSQLRest instance shall call this method when a record is retrieved,
  12299. // added or updated
  12300. // - this overloaded method expects the content to be specified as JSON object,
  12301. // and TSQLRecordClass to be specified as its index in Rest.Model.Tables[]
  12302. procedure Notify(aTableIndex: integer; aID: TID; const aJSON: RawUTF8;
  12303. aAction: TSQLOccasion); overload;
  12304. /// TSQLRest instance shall call this method when a record is added or updated
  12305. // - this overloaded method will call the other Trace method, serializing
  12306. // the supplied aRecord content as JSON (not in the case of seDelete)
  12307. procedure Notify(aRecord: TSQLRecord; aAction: TSQLOccasion); overload;
  12308. /// TSQLRest instance shall call this method when a record is deleted
  12309. // - this method is dedicated for a record deletion
  12310. procedure NotifyDeletion(aTable: TSQLRecordClass; aID: TID); overload;
  12311. /// TSQLRest instance shall call this method when a record is deleted
  12312. // - this method is dedicated for a record deletion
  12313. // - TSQLRecordClass to be specified as its index in Rest.Model.Tables[]
  12314. procedure NotifyDeletion(aTableIndex, aID: TID); overload;
  12315. end;
  12316. /// how a TSQLRest class may execute read or write operations
  12317. // - used e.g. for TSQLRestServer.AcquireWriteMode or
  12318. // TSQLRestServer.AcquireExecutionMode/AcquireExecutionLockedTimeOut
  12319. TSQLRestServerAcquireMode = (
  12320. amUnlocked, amLocked, amBackgroundThread, amBackgroundORMSharedThread
  12321. {$ifndef LVCL}, amMainThread{$endif});
  12322. /// class-reference type (metaclass) of a TSQLRest kind
  12323. TSQLRestClass = class of TSQLRest;
  12324. /// a dynamic array of TSQLRest instances
  12325. TSQLRestDynArray = array of TSQLRest;
  12326. /// a dynamic array of TSQLRest instances, owniing the instances
  12327. TSQLRestObjArray = array of TSQLRest;
  12328. /// used to store the execution parameters for a TSQLRest instance
  12329. TSQLRestAcquireExecution = class(TSynPersistentLocked)
  12330. public
  12331. /// how read or write operations will be executed
  12332. Mode: TSQLRestServerAcquireMode;
  12333. /// delay before failing to acquire the lock
  12334. LockedTimeOut: cardinal;
  12335. /// background thread instance (if any)
  12336. Thread: TSynBackgroundThreadMethod;
  12337. /// finalize the memory structure, and the associated background thread
  12338. destructor Destroy; override;
  12339. end;
  12340. /// a generic REpresentational State Transfer (REST) client/server class
  12341. TSQLRest = class
  12342. protected
  12343. fModel: TSQLModel;
  12344. fCache: TSQLRestCache;
  12345. fTransactionActiveSession: cardinal;
  12346. fTransactionTable: TSQLRecordClass;
  12347. fServerTimeStampOffset: TDateTime;
  12348. fServerTimeStampCacheTix: cardinal;
  12349. fServerTimeStampCacheValue: TTimeLogBits;
  12350. fServices: TServiceContainer;
  12351. fPrivateGarbageCollector: TObjectList;
  12352. fRoutingClass: TSQLRestServerURIContextClass;
  12353. fFrequencyTimeStamp: Int64;
  12354. fAcquireExecution: array[TSQLRestServerURIContextCommand] of TSQLRestAcquireExecution;
  12355. {$ifdef WITHLOG}
  12356. fLogClass: TSynLogClass; // =SQLite3Log by default
  12357. fLogFamily: TSynLogFamily; // =SQLite3Log.Family by default
  12358. procedure SetLogClass(aClass: TSynLogClass); virtual;
  12359. function GetLogClass: TSynLogClass;
  12360. {$endif}
  12361. /// log the corresponding text (if logging is enabled)
  12362. procedure InternalLog(const Text: RawUTF8; Level: TSynLogInfo); overload;
  12363. {$ifdef HASINLINE}inline;{$endif}
  12364. procedure InternalLog(const Format: RawUTF8; const Args: array of const;
  12365. Level: TSynLogInfo); overload;
  12366. /// internal method used by Delete(Table,SQLWhere) method
  12367. function InternalDeleteNotifyAndGetIDs(Table: TSQLRecordClass; const SQLWhere: RawUTF8;
  12368. var IDs: TIDDynArray): boolean;
  12369. /// retrieve the server time stamp
  12370. // - default implementation will use fServerTimeStampOffset to compute
  12371. // the value from PC time (i.e. NowUTC+fServerTimeStampOffset as TTimeLog)
  12372. // - inherited classes may override this method, or set the appropriate
  12373. // value in fServerTimeStampOffset protected field
  12374. function GetServerTimeStamp: TTimeLog; virtual;
  12375. /// compute the server time stamp offset from the given
  12376. procedure SetServerTimeStamp(const Value: TTimeLog);
  12377. /// handle Client or Server side fast in-memory cache
  12378. // - creates the internal fCache instance, if necessary
  12379. function GetCache: TSQLRestCache;
  12380. {$ifdef HASINLINE}inline;{$endif}
  12381. /// returns TRUE if this table is worth caching (e.g. not in memory)
  12382. // - this default implementation always returns TRUE (always allow cache)
  12383. function CacheWorthItForTable(aTableIndex: cardinal): boolean; virtual;
  12384. /// compute SELECT ... FROM TABLE WHERE ...
  12385. function SQLComputeForSelect(Table: TSQLRecordClass;
  12386. const FieldNames, WhereClause: RawUTF8): RawUTF8;
  12387. /// wrapper method for RoutingClass property
  12388. procedure SetRoutingClass(aServicesRouting: TSQLRestServerURIContextClass);
  12389. /// wrapper methods to access fAcquireExecution[]
  12390. function GetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand): TSQLRestServerAcquireMode;
  12391. procedure SetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand; Value: TSQLRestServerAcquireMode);
  12392. function GetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand): cardinal;
  12393. procedure SetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand; Value: cardinal);
  12394. /// internal method called by TSQLRestServer.Batch() to process fast sending
  12395. // to remote database engine (e.g. Oracle bound arrays or MS SQL Bulk insert)
  12396. // - returns TRUE if this method is handled by the engine, or FALSE if
  12397. // individual calls to Engine*() are expected
  12398. // - this default implementation returns FALSE
  12399. // - an overridden method returning TRUE shall ensure that calls to
  12400. // EngineAdd / EngineUpdate / EngineDelete (depending of supplied Method)
  12401. // will properly handle operations until InternalBatchStop() is called
  12402. function InternalBatchStart(Method: TSQLURIMethod;
  12403. BatchOptions: TSQLRestBatchOptions): boolean; virtual;
  12404. /// internal method called by TSQLRestServer.Batch() to process fast sending
  12405. // to remote database engine (e.g. Oracle bound arrays or MS SQL Bulk insert)
  12406. // - this default implementation will raise an EORMException (since
  12407. // InternalBatchStart returns always FALSE at this TSQLRest level)
  12408. // - InternalBatchStart/Stop may safely use a lock for multithreading:
  12409. // implementation in TSQLRestServer.Batch use a try..finally block
  12410. procedure InternalBatchStop; virtual;
  12411. /// send/execute the supplied JSON BATCH content, and return the expected array
  12412. // - this method will be implemented for TSQLRestClient and TSQLRestServer only
  12413. // - this default implementation will trigger an EORMException
  12414. function EngineBatchSend(Table: TSQLRecordClass; const Data: RawUTF8;
  12415. var Results: TIDDynArray; ExpectedResultsCount: integer): integer; virtual;
  12416. /// any overriden TSQLRest class should call it in the initialization section
  12417. class procedure RegisterClassNameForDefinition;
  12418. // inherited classes should unserialize the other aDefinition properties by
  12419. // overriding this method, in a reverse logic to overriden DefinitionTo()
  12420. constructor RegisteredClassCreateFrom(aModel: TSQLModel;
  12421. aDefinition: TSynConnectionDefinition); virtual;
  12422. /// used by Add() and AddWithBlobs() before EngineAdd()
  12423. procedure GetJSONValuesForAdd(TableIndex: integer; Value: TSQLRecord;
  12424. ForceID, DoNotAutoComputeFields, WithBlobs: boolean;
  12425. CustomFields: PSQLFieldBits; var result: RawUTF8);
  12426. /// used by all overloaded Add() methods
  12427. function InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits;
  12428. ForceID, DoNotAutoComputeFields: boolean): TID; virtual;
  12429. protected // these abstract methods must be overriden by real database engine
  12430. /// retrieve a list of members as JSON encoded data
  12431. // - implements REST GET collection
  12432. // - returns '' on error, or JSON data, even with no result rows
  12433. // - override this method for direct data retrieval from the database engine
  12434. // and direct JSON export, avoiding a TSQLTable which allocates memory for every
  12435. // field values before the JSON export
  12436. // - can be called for a single Table (ModelRoot/Table), or with low level SQL
  12437. // query (ModelRoot + SQL sent as request body)
  12438. // - if ReturnedRowCount points to an integer variable, it must be filled with
  12439. // the number of row data returned (excluding field names)
  12440. // - this method must be implemented in a thread-safe manner
  12441. function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false;
  12442. ReturnedRowCount: PPtrInt=nil): RawUTF8; virtual; abstract;
  12443. /// Execute directly a SQL statement, without any result
  12444. // - implements POST SQL on ModelRoot URI
  12445. // - return true on success
  12446. // - override this method for proper calling the database engine
  12447. // - don't call this method in normal cases
  12448. // - this method must be implemented to be thread-safe
  12449. function EngineExecute(const aSQL: RawUTF8): boolean; virtual; abstract;
  12450. /// get a member from its ID
  12451. // - implements REST GET member
  12452. // - returns the data of this object as JSON
  12453. // - override this method for proper data retrieval from the database engine
  12454. // - this method must be implemented in a thread-safe manner
  12455. function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; virtual; abstract;
  12456. /// create a new member
  12457. // - implements REST POST collection
  12458. // - SentData can contain the JSON object with field values to be added
  12459. // - class is taken from Model.Tables[TableModelIndex]
  12460. // - returns the TSQLRecord ID/ROWID value, 0 on error
  12461. // - if a "RowID":.. or "ID":.. member is set in SentData, it shall force
  12462. // this value as insertion ID
  12463. // - override this method for proper calling the database engine
  12464. // - this method must be implemented in a thread-safe manner
  12465. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; virtual; abstract;
  12466. /// update a member
  12467. // - implements REST PUT collection
  12468. // - SentData can contain the JSON object with field values to be added
  12469. // - returns true on success
  12470. // - override this method for proper calling the database engine
  12471. // - this method must be implemented in a thread-safe manner
  12472. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; virtual; abstract;
  12473. /// delete a member
  12474. // - implements REST DELETE collection
  12475. // - returns true on success
  12476. // - override this method for proper calling the database engine
  12477. // - this method must be implemented in a thread-safe manner
  12478. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; virtual; abstract;
  12479. /// delete several members, from a WHERE clause
  12480. // - IDs[] contains the already-computed matching IDs for SQLWhere
  12481. // - returns true on success
  12482. // - override this method for proper calling the database engine, i.e.
  12483. // using either IDs[] or a faster SQL statement
  12484. // - this method must be implemented in a thread-safe manner
  12485. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  12486. const IDs: TIDDynArray): boolean; virtual; abstract;
  12487. /// get a blob field content from its member ID and field name
  12488. // - implements REST GET member with a supplied blob field name
  12489. // - returns TRUE on success
  12490. // - returns the data of this blob as raw binary (not JSON) in BlobData
  12491. // - override this method for proper data retrieval from the database engine
  12492. // - this method must be implemented in a thread-safe manner
  12493. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  12494. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; virtual; abstract;
  12495. /// update a blob field content from its member ID and field name
  12496. // - implements REST PUT member with a supplied blob field name
  12497. // - returns TRUE on success
  12498. // - the data of this blob must be specified as raw binary (not JSON) in BlobData
  12499. // - override this method for proper data retrieval from the database engine
  12500. // - this method must be implemented in a thread-safe manner
  12501. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  12502. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; virtual; abstract;
  12503. /// update an individual record field value from a specified ID or Value
  12504. // - return true on success
  12505. // - will allow execution of requests like
  12506. // $ UPDATE tablename SET setfieldname=setvalue WHERE wherefieldname=wherevalue
  12507. // - SetValue and WhereValue parameters must match our inline format, i.e.
  12508. // by double quoted with " for strings, or be plain text for numbers - e.g.
  12509. // $ Client.EngineUpdateField(TSQLMyRecord,'FirstName','"Smith"','RowID','10')
  12510. // but you should better use the UpdateField() overload methods instead
  12511. // - WhereFieldName and WhereValue must be set: for security reasons,
  12512. // implementations of this method will reject an UPDATE without any WHERE
  12513. // clause, so you won't be able to use it to execute such statements:
  12514. // $ UPDATE tablename SET setfieldname=setvalue
  12515. // - this method must be implemented in a thread-safe manner
  12516. function EngineUpdateField(TableModelIndex: integer;
  12517. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; virtual; abstract;
  12518. /// increments one integer field value
  12519. // - this default implementation is just a wrapper around OneFieldValue +
  12520. // UpdateField methods
  12521. function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  12522. const FieldName: RawUTF8; Increment: Int64): boolean; virtual;
  12523. function GetCurrentSessionUserID: TID; virtual; abstract;
  12524. public
  12525. /// initialize the class, and associate it to a specified database Model
  12526. constructor Create(aModel: TSQLModel); virtual;
  12527. /// release internal used instances
  12528. // - e.g. release associated TSQLModel or TServiceContainer
  12529. destructor Destroy; override;
  12530. /// save the TSQLRest properties into a persistent storage object
  12531. // - you can then use TSQLRest.CreateFrom() to re-instantiate it
  12532. // - current Definition.Key value will be used for the password encryption
  12533. // - this default implementation will set the class name in Definition.Kind:
  12534. // inherited classes should override this method and serialize other
  12535. // properties, then override RegisteredClassCreateFrom() protected method
  12536. // to initiate the very same instance
  12537. procedure DefinitionTo(Definition: TSynConnectionDefinition); virtual;
  12538. /// save the properties into a JSON file
  12539. // - you can then use TSQLRest.CreateFromJSON() to re-instantiate it
  12540. // - you can specify a custom Key, if the default is not enough for you
  12541. function DefinitionToJSON(Key: cardinal=0): RawUTF8;
  12542. /// save the properties into a JSON file
  12543. // - you can then use TSQLRest.CreateFromFile() to re-instantiate it
  12544. // - you can specify a custom Key, if the default is not enough for you
  12545. procedure DefinitionToFile(const aJSONFile: TFileName; aKey: cardinal=0);
  12546. /// create a new TSQLRest instance from its Model and stored values
  12547. // - aDefinition.Kind will define the actual class which will be
  12548. // instantiated: currently TSQLRestServerFullMemory, TSQLRestServerDB,
  12549. // TSQLRestClientURINamedPipe, TSQLRestClientURIMessage,
  12550. // TSQLHttpClientWinSock, TSQLHttpClientWinINet, TSQLHttpClientWinHTTP,
  12551. // and TSQLHttpClientCurl classes are recognized by this method
  12552. // - then other aDefinition fields will be used to refine the instance:
  12553. // please refer to each overriden DefinitionTo() method documentation
  12554. // - use TSQLRestMongoDBCreate() and/or TSQLRestExternalDBCreate() instead
  12555. // to create a TSQLRest instance will all tables defined as external when
  12556. // aDefinition.Kind is 'MongoDB' or a TSQLDBConnectionProperties class
  12557. // - will raise an exception if the supplied definition are not valid
  12558. class function CreateFrom(aModel: TSQLModel;
  12559. aDefinition: TSynConnectionDefinition): TSQLRest;
  12560. /// try to create a new TSQLRest instance from its Model and stored values
  12561. // - will return nil if the supplied definition are not valid
  12562. // - if the newly created instance is a TSQLRestServer, will force the
  12563. // supplied aServerHandleAuthentication parameter to enable authentication
  12564. class function CreateTryFrom(aModel: TSQLModel;
  12565. aDefinition: TSynConnectionDefinition;
  12566. aServerHandleAuthentication: boolean): TSQLRest;
  12567. /// create a new TSQLRest instance from its Model and JSON stored values
  12568. // - aDefinition.Kind will define the actual class which will be instantiated
  12569. // - you can specify a custom Key, if the default is not safe enough for you
  12570. class function CreateFromJSON(aModel: TSQLModel;
  12571. const aJSONDefinition: RawUTF8; aKey: cardinal=0): TSQLRest;
  12572. /// create a new TSQLRest instance from its Model and a JSON file
  12573. // - aDefinition.Kind will define the actual class which will be instantiated
  12574. // - you can specify a custom Key, if the default is not safe enough for you
  12575. class function CreateFromFile(aModel: TSQLModel;
  12576. const aJSONFile: TFileName; aKey: cardinal=0): TSQLRest;
  12577. /// retrieve the registered class from the aDefinition.Kind string
  12578. class function ClassFrom(aDefinition: TSynConnectionDefinition): TSQLRestClass;
  12579. {$ifdef WITHLOG}
  12580. /// the logging family used for this instance
  12581. // - is set by default to SQLite3Log.Family, but could be set to something
  12582. // else by setting a custom class to the LogClass property
  12583. property LogFamily: TSynLogFamily read fLogFamily;
  12584. {$endif}
  12585. /// a local "Garbage collector" list, for some classes instances which must
  12586. // live during the whole TSQLRestServer process
  12587. // - is used internally by the class, but can be used for business code
  12588. property PrivateGarbageCollector: TObjectList read fPrivateGarbageCollector;
  12589. public
  12590. /// get the row count of a specified table
  12591. // - returns -1 on error
  12592. // - returns the row count of the table on success
  12593. // - calls internaly the "SELECT Count(*) FROM TableName;" SQL statement
  12594. function TableRowCount(Table: TSQLRecordClass): Int64; virtual;
  12595. /// check if there is some data rows in a specified table
  12596. // - calls internaly a "SELECT RowID FROM TableName LIMIT 1" SQL statement,
  12597. // which is much faster than testing if "SELECT count(*)" equals 0 - see
  12598. // @http://stackoverflow.com/questions/8988915
  12599. function TableHasRows(Table: TSQLRecordClass): boolean; virtual;
  12600. /// search for the last inserted ID in a specified table
  12601. // - returns -1 on error
  12602. // - will execute by default "SELECT max(rowid) FROM TableName"
  12603. function TableMaxID(Table: TSQLRecordClass): TID; virtual;
  12604. /// check if a given ID do exist for a given table
  12605. function MemberExists(Table: TSQLRecordClass; ID: TID): boolean;
  12606. /// get the UTF-8 encoded value of an unique field with a Where Clause
  12607. // - example of use - including inlined parameters via :(...):
  12608. // ! aClient.OneFieldValue(TSQLRecord,'Name','ID=:(23):')
  12609. // you should better call the corresponding overloaded method as such:
  12610. // ! aClient.OneFieldValue(TSQLRecord,'Name','ID=?',[aID])
  12611. // which is the same as calling:
  12612. // ! aClient.OneFieldValue(TSQLRecord,'Name',FormatUTF8('ID=?',[],[23]))
  12613. // - call internaly ExecuteList() to get the value
  12614. function OneFieldValue(Table: TSQLRecordClass;
  12615. const FieldName, WhereClause: RawUTF8): RawUTF8; overload;
  12616. /// get the UTF-8 encoded value of an unique field with a Where Clause
  12617. // - this overloaded function will call FormatUTF8 to create the Where Clause
  12618. // from supplied parameters, binding all '?' chars with Args[] values
  12619. // - example of use:
  12620. // ! aClient.OneFieldValue(TSQLRecord,'Name','ID=?',[aID])
  12621. // - call internaly ExecuteList() to get the value
  12622. // - note that this method prototype changed with revision 1.17 of the
  12623. // framework: array of const used to be Args and '%' in the FormatSQLWhere
  12624. // statement, whereas it now expects bound parameters as '?'
  12625. function OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8;
  12626. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): RawUTF8; overload;
  12627. /// get the UTF-8 encoded value of an unique field with a Where Clause
  12628. // - this overloaded function will call FormatUTF8 to create the Where Clause
  12629. // from supplied parameters, replacing all '%' chars with Args[], and all '?'
  12630. // chars with Bounds[] (inlining them with :(...): and auto-quoting strings)
  12631. // - example of use:
  12632. // ! OneFieldValue(TSQLRecord,'Name','%=?',['ID'],[aID])
  12633. // - call internaly ExecuteList() to get the value
  12634. function OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8;
  12635. const WhereClauseFmt: RawUTF8; const Args, Bounds: array of const): RawUTF8; overload;
  12636. /// get one integer value of an unique field with a Where Clause
  12637. // - this overloaded function will return the field value as integer
  12638. function OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8;
  12639. const WhereClauseFmt: RawUTF8; const Args, Bounds: array of const;
  12640. out Data: Int64): boolean; overload;
  12641. /// get the UTF-8 encoded value of an unique field from its ID
  12642. // - example of use: OneFieldValue(TSQLRecord,'Name',23)
  12643. // - call internaly ExecuteList() to get the value
  12644. function OneFieldValue(Table: TSQLRecordClass;
  12645. const FieldName: RawUTF8; WhereID: TID): RawUTF8; overload;
  12646. /// get the UTF-8 encoded value of some fields with a Where Clause
  12647. // - example of use: MultiFieldValue(TSQLRecord,['Name'],Name,'ID=:(23):')
  12648. // (using inlined parameters via :(...): is always a good idea)
  12649. // - FieldValue[] will have the same length as FieldName[]
  12650. // - return true on success, false on SQL error or no result
  12651. // - call internaly ExecuteList() to get the list
  12652. function MultiFieldValue(Table: TSQLRecordClass;
  12653. const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8;
  12654. const WhereClause: RawUTF8): boolean; overload;
  12655. /// get the UTF-8 encoded value of some fields from its ID
  12656. // - example of use: MultiFieldValue(TSQLRecord,['Name'],Name,23)
  12657. // - FieldValue[] will have the same length as FieldName[]
  12658. // - return true on success, false on SQL error or no result
  12659. // - call internaly ExecuteList() to get the list
  12660. function MultiFieldValue(Table: TSQLRecordClass;
  12661. const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8;
  12662. WhereID: TID): boolean; overload;
  12663. /// get the UTF-8 encoded values of an unique field with a Where Clause
  12664. // - example of use: OneFieldValue(TSQLRecord,'FirstName','Name=:("Smith"):',Data)
  12665. // (using inlined parameters via :(...): is always a good idea)
  12666. // - leave WhereClause void to get all records
  12667. // - call internaly ExecuteList() to get the list
  12668. // - returns TRUE on success, FALSE if no data was retrieved
  12669. function OneFieldValues(Table: TSQLRecordClass; const FieldName: RawUTF8;
  12670. const WhereClause: RawUTF8; out Data: TRawUTF8DynArray): boolean; overload;
  12671. /// get the integer value of an unique field with a Where Clause
  12672. // - example of use: OneFieldValue(TSQLRecordPeople,'ID','Name=:("Smith"):',Data)
  12673. // (using inlined parameters via :(...): is always a good idea)
  12674. // - leave WhereClause void to get all records
  12675. // - call internaly ExecuteList() to get the list
  12676. function OneFieldValues(Table: TSQLRecordClass; const FieldName: RawUTF8;
  12677. const WhereClause: RawUTF8; var Data: TInt64DynArray; SQL: PRawUTF8=nil): boolean; overload;
  12678. /// dedicated method used to retrieve free-text matching DocIDs
  12679. // - this method will work for both TSQLRecordFTS3 and TSQLRecordFTS4
  12680. // - this method expects the column/field names to be supplied in the MATCH
  12681. // statement clause
  12682. // - example of use: FTSMatch(TSQLMessage,'Body MATCH :("linu*"):',IntResult)
  12683. // (using inlined parameters via :(...): is always a good idea)
  12684. function FTSMatch(Table: TSQLRecordFTS3Class; const WhereClause: RawUTF8;
  12685. var DocID: TIDDynArray): boolean; overload;
  12686. /// dedicated method used to retrieve free-text matching DocIDs with
  12687. // enhanced ranking information
  12688. // - this method will work for both TSQLRecordFTS3 and TSQLRecordFTS4
  12689. // - this method will search in all FTS3 columns, and except some floating-point
  12690. // constants for weigthing each column (there must be the same number of
  12691. // PerFieldWeight parameters as there are columns in the TSQLRecordFTS3 table)
  12692. // - example of use: FTSMatch(TSQLDocuments,'"linu*"',IntResult,[1,0.5])
  12693. // which will sort the results by the rank obtained with the 1st column/field
  12694. // beeing given twice the weighting of those in the 2nd (and last) column
  12695. // - FTSMatch(TSQLDocuments,'linu*',IntResult,[1,0.5]) will perform a
  12696. // SQL query as such, which is the fastest way of ranking according to
  12697. // http://www.sqlite.org/fts3.html#appendix_a
  12698. // $ SELECT RowID FROM Documents WHERE Documents MATCH 'linu*'
  12699. // $ ORDER BY rank(matchinfo(Documents),1.0,0.5) DESC
  12700. function FTSMatch(Table: TSQLRecordFTS3Class; const MatchClause: RawUTF8;
  12701. var DocID: TIDDynArray; const PerFieldWeight: array of double;
  12702. limit: integer=0; offset: integer=0): boolean; overload;
  12703. /// get the CSV-encoded UTF-8 encoded values of an unique field with a Where Clause
  12704. // - example of use: OneFieldValue(TSQLRecord,'FirstName','Name=:("Smith")',Data)
  12705. // (using inlined parameters via :(...): is always a good idea)
  12706. // - leave WhereClause void to get all records
  12707. // - call internaly ExecuteList() to get the list
  12708. // - using inlined parameters via :(...): in WhereClause is always a good idea
  12709. function OneFieldValues(Table: TSQLRecordClass; const FieldName: RawUTF8;
  12710. const WhereClause: RawUTF8=''; const Separator: RawUTF8=','): RawUTF8; overload;
  12711. /// get the string-encoded values of an unique field into some TStrings
  12712. // - Items[] will be filled with string-encoded values of the given field)
  12713. // - Objects[] will be filled with pointer(ID)
  12714. // - call internaly ExecuteList() to get the list
  12715. // - returns TRUE on success, FALSE if no data was retrieved
  12716. // - if IDToIndex is set, its value will be replaced with the index in
  12717. // Strings.Objects[] where ID=IDToIndex^
  12718. // - using inlined parameters via :(...): in WhereClause is always a good idea
  12719. function OneFieldValues(Table: TSQLRecordClass;
  12720. const FieldName, WhereClause: RawUTF8; Strings: TStrings;
  12721. IDToIndex: PID=nil): Boolean; overload;
  12722. /// Execute directly a SQL statement, expecting a list of resutls
  12723. // - return a result table on success, nil on failure
  12724. // - FieldNames can be the CSV list of field names to be retrieved
  12725. // - if FieldNames is '', will get all simple fields, excluding BLOBs
  12726. // - if FieldNames is '*', will get ALL fields, including ID and BLOBs
  12727. // - call internaly ExecuteList() to get the list
  12728. // - using inlined parameters via :(...): in WhereClause is always a good idea
  12729. function MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8;
  12730. const WhereClause: RawUTF8=''): TSQLTableJSON; overload; virtual;
  12731. /// Execute directly a SQL statement, expecting a list of resutls
  12732. // - return a result table on success, nil on failure
  12733. // - FieldNames can be the CSV list of field names to be retrieved
  12734. // - if FieldNames is '', will get all simple fields, excluding BLOBs
  12735. // - if FieldNames is '*', will get ALL fields, including ID and BLOBs
  12736. // - this overloaded function will call FormatUTF8 to create the Where Clause
  12737. // from supplied parameters, binding all '?' chars with Args[] values
  12738. // - example of use:
  12739. // ! aList := aClient.MultiFieldValues(TSQLRecord,'Name,FirstName','Salary>=?',[aMinSalary]);
  12740. // - call overloaded MultiFieldValues() / ExecuteList() to get the list
  12741. // - note that this method prototype changed with revision 1.17 of the
  12742. // framework: array of const used to be Args and '%' in the WhereClauseFormat
  12743. // statement, whereas it now expects bound parameters as '?'
  12744. function MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8;
  12745. const WhereClauseFormat: RawUTF8; const BoundsSQLWhere: array of const): TSQLTableJSON; overload;
  12746. /// Execute directly a SQL statement, expecting a list of results
  12747. // - return a result table on success, nil on failure
  12748. // - FieldNames can be the CSV list of field names to be retrieved
  12749. // - if FieldNames is '', will get all simple fields, excluding BLOBs
  12750. // - if FieldNames is '*', will get ALL fields, including ID and BLOBs
  12751. // - in this version, the WHERE clause can be created with the same format
  12752. // as FormatUTF8() function, replacing all '%' chars with Args[], and all '?'
  12753. // chars with Bounds[] (inlining them with :(...): and auto-quoting strings)
  12754. // - example of use:
  12755. // ! Table := MultiFieldValues(TSQLRecord,'Name','%=?',['ID'],[aID]);
  12756. // - call overloaded MultiFieldValues() / ExecuteList() to get the list
  12757. function MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8;
  12758. const WhereClauseFormat: RawUTF8; const Args, Bounds: array of const): TSQLTableJSON; overload;
  12759. /// retrieve the main field (mostly 'Name') value of the specified record
  12760. // - use GetMainFieldName() method to get the main field name
  12761. // - use OneFieldValue() method to get the field value
  12762. // - return '' if no such field or record exists
  12763. // - if ReturnFirstIfNoUnique is TRUE and no unique property is found,
  12764. // the first RawUTF8 property is returned anyway
  12765. function MainFieldValue(Table: TSQLRecordClass; ID: TID;
  12766. ReturnFirstIfNoUnique: boolean=false): RawUTF8;
  12767. /// return the ID of the record which main field match the specified value
  12768. // - search field is mainly the "Name" property, i.e. the one with
  12769. // "stored AS_UNIQUE" (i.e. "stored false") definition on most TSQLRecord
  12770. // - returns 0 if no matching record was found }
  12771. function MainFieldID(Table: TSQLRecordClass; const Value: RawUTF8): TID;
  12772. /// return the IDs of the record which main field match the specified values
  12773. // - search field is mainly the "Name" property, i.e. the one with
  12774. // "stored AS_UNIQUE" (i.e. "stored false") definition on most TSQLRecord
  12775. // - if any of the Values[] is not existing, then no ID will appear in the
  12776. // IDs[] array - e.g. it will return [] if no matching record was found
  12777. // - returns TRUE if any matching ID was found (i.e. if length(IDs)>0) }
  12778. function MainFieldIDs(Table: TSQLRecordClass; const Values: array of RawUTF8;
  12779. out IDs: TIDDynArray): boolean;
  12780. public // here are REST basic direct calls (works with Server or Client)
  12781. /// get a member from a SQL statement
  12782. // - implements REST GET collection
  12783. // - return true on success
  12784. // - Execute 'SELECT * FROM TableName WHERE SQLWhere LIMIT 1' SQL Statememt
  12785. // (using inlined parameters via :(...): in SQLWhere is always a good idea)
  12786. // - since no record is specified, locking is pointless here
  12787. // - default implementation call ExecuteList(), and fill Value from a
  12788. // temporary TSQLTable
  12789. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12790. // and TSQLRecordMany fields (use RetrieveBlob method or set
  12791. // TSQLRestClientURI.ForceBlobTransfert)
  12792. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12793. // - if this default set of simple fields does not fit your need, you could
  12794. // specify your own set
  12795. function Retrieve(const SQLWhere: RawUTF8; Value: TSQLRecord;
  12796. const aCustomFieldsCSV: RawUTF8=''): boolean; overload; virtual;
  12797. /// get a member from a SQL statement
  12798. // - implements REST GET collection
  12799. // - return true on success
  12800. // - same as Retrieve(const SQLWhere: RawUTF8; Value: TSQLRecord) method, but
  12801. // this overloaded function will call FormatUTF8 to create the Where Clause
  12802. // from supplied parameters, replacing all '%' chars with Args[], and all '?'
  12803. // chars with Bounds[] (inlining them with :(...): and auto-quoting strings)
  12804. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12805. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12806. function Retrieve(const WhereClauseFmt: RawUTF8; const Args,Bounds: array of const;
  12807. Value: TSQLRecord; const aCustomFieldsCSV: RawUTF8=''): boolean; overload;
  12808. /// get a member from its ID
  12809. // - return true on success
  12810. // - Execute 'SELECT * FROM TableName WHERE ID=:(aID): LIMIT 1' SQL Statememt
  12811. // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock
  12812. // the corresponding record, then retrieve its content; caller has to call
  12813. // UnLock() method after Value usage, to release the record
  12814. // - this method will call EngineRetrieve() abstract method
  12815. // - the TSQLRawBlob (BLOB) fields are not retrieved by this method, to
  12816. // preserve bandwidth: use the RetrieveBlob() methods for handling
  12817. // BLOB fields, or set either the TSQLRestClientURI.ForceBlobTransfert
  12818. // or TSQLRestClientURI.ForceBlobTransfertTable[] properties
  12819. // - the TSQLRecordMany fields are not retrieved either: they are separate
  12820. // instances created by TSQLRecordMany.Create, with dedicated methods to
  12821. // access to the separated pivot table
  12822. function Retrieve(aID: TID; Value: TSQLRecord;
  12823. ForUpdate: boolean=false): boolean; overload; virtual;
  12824. /// get a member from its TRecordReference property content
  12825. // - instead of the other Retrieve() methods, this implementation Create an
  12826. // instance, with the appropriated class stored in Reference
  12827. // - returns nil on any error (invalid Reference e.g.)
  12828. // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock
  12829. // the corresponding record, then retrieve its content; caller has to call
  12830. // UnLock() method after Value usage, to release the record
  12831. // - the TSQLRawBlob (BLOB) fields are not retrieved by this method, to
  12832. // preserve bandwidth: use the RetrieveBlob() methods for handling
  12833. // BLOB fields, or set either the TSQLRestClientURI.ForceBlobTransfert
  12834. // or TSQLRestClientURI.ForceBlobTransfertTable[] properties
  12835. // - the TSQLRecordMany fields are not retrieved either: they are separate
  12836. // instances created by TSQLRecordMany.Create, with dedicated methods to
  12837. // access to the separated pivot table
  12838. function Retrieve(Reference: TRecordReference;
  12839. ForUpdate: boolean=false): TSQLRecord; overload; virtual;
  12840. /// get a member from a published property TSQLRecord
  12841. // - those properties are not class instances, but TObject(aRecordID)
  12842. // - is just a wrapper around Retrieve(aPublishedRecord.ID,aValue)
  12843. // - return true on success
  12844. function Retrieve(aPublishedRecord, aValue: TSQLRecord): boolean; overload;
  12845. /// get a list of members from a SQL statement as TObjectList
  12846. // - implements REST GET collection
  12847. // - for better server speed, the WHERE clause should use bound parameters
  12848. // identified as '?' in the FormatSQLWhere statement, which is expected to
  12849. // follow the order of values supplied in BoundsSQLWhere open array - use
  12850. // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer,
  12851. // double, currency, RawUTF8 values to be bound to the request as parameters
  12852. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  12853. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12854. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12855. // - return a TObjectList on success (possibly with Count=0) - caller is
  12856. // responsible of freeing the instance
  12857. // - this TObjectList will contain a list of all matching records
  12858. // - return nil on error
  12859. function RetrieveList(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8;
  12860. const BoundsSQLWhere: array of const;
  12861. const aCustomFieldsCSV: RawUTF8=''): TObjectList; overload;
  12862. /// get a list of members from a SQL statement as RawJSON
  12863. // - implements REST GET collection
  12864. // - for better server speed, the WHERE clause should use bound parameters
  12865. // identified as '?' in the FormatSQLWhere statement, which is expected to
  12866. // follow the order of values supplied in BoundsSQLWhere open array - use
  12867. // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer,
  12868. // double, currency, RawUTF8 values to be bound to the request as parameters
  12869. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  12870. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12871. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12872. // - returns the raw JSON array content with all items on success, with
  12873. // our expanded / not expanded JSON format - so can be used with SOA methods
  12874. // and RawJSON results, for direct process from the client side
  12875. // - returns '' on error
  12876. // - the data is directly retrieved from raw JSON as returned by the database
  12877. // without any conversion, so this method would be the fastest, but complex
  12878. // types like dynamic array would be returned as Base64-encoded blob value -
  12879. // if you need proper JSON access to those, see RetrieveDocVariantArray()
  12880. function RetrieveListJSON(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8;
  12881. const BoundsSQLWhere: array of const;
  12882. const aCustomFieldsCSV: RawUTF8=''; aForceAJAX: boolean=false): RawJSON; overload;
  12883. /// get a list of members from a SQL statement as RawJSON
  12884. // - implements REST GET collection
  12885. // - this overloaded version expect the SQLWhere clause to be already
  12886. // prepared with inline parameters using a previous FormatUTF8() call
  12887. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  12888. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12889. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12890. // - returns the raw JSON array content with all items on success, with
  12891. // our expanded / not expanded JSON format - so can be used with SOA methods
  12892. // and RawJSON results, for direct process from the client side
  12893. // - returns '' on error
  12894. // - the data is directly retrieved from raw JSON as returned by the database
  12895. // without any conversion, so this method would be the fastest, but complex
  12896. // types like dynamic array would be returned as Base64-encoded blob value -
  12897. // if you need proper JSON access to those, see RetrieveDocVariantArray()
  12898. function RetrieveListJSON(Table: TSQLRecordClass; const SQLWhere: RawUTF8;
  12899. const aCustomFieldsCSV: RawUTF8=''; aForceAJAX: boolean=false): RawJSON; overload;
  12900. {$ifndef NOVARIANTS}
  12901. /// get a list of all members from a SQL statement as a TDocVariant
  12902. // - implements REST GET collection
  12903. // - if ObjectName='', it will return a TDocVariant of dvArray kind
  12904. // - if ObjectName is set, it will return a TDocVariant of dvObject kind,
  12905. // with one property containing the array of values: this returned variant
  12906. // can be pasted e.g. directly as parameter to TSynMustache.Render()
  12907. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  12908. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12909. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12910. // - the data will be converted to variants and TDocVariant following the
  12911. // TSQLRecord layout, so complex types like dynamic array would be returned
  12912. // as a true array of values (in contrast to the RetrieveListJSON method)
  12913. function RetrieveDocVariantArray(Table: TSQLRecordClass;
  12914. const ObjectName, CustomFieldsCSV: RawUTF8; FirstRecordID: PID=nil;
  12915. LastRecordID: PID=nil): variant; overload;
  12916. {$ifdef HASINLINE}inline;{$endif}
  12917. /// get a list of members from a SQL statement as a TDocVariant
  12918. // - implements REST GET collection over a specified WHERE clause
  12919. // - if ObjectName='', it will return a TDocVariant of dvArray kind
  12920. // - if ObjectName is set, it will return a TDocVariant of dvObject kind,
  12921. // with one property containing the array of values: this returned variant
  12922. // can be pasted e.g. directly as parameter to TSynMustache.Render()
  12923. // - for better server speed, the WHERE clause should use bound parameters
  12924. // identified as '?' in the FormatSQLWhere statement, which is expected to
  12925. // follow the order of values supplied in BoundsSQLWhere open array - use
  12926. // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer,
  12927. // double, currency, RawUTF8 values to be bound to the request as parameters
  12928. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  12929. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12930. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12931. // - the data will be converted to variants and TDocVariant following the
  12932. // TSQLRecord layout, so complex types like dynamic array would be returned
  12933. // as a true array of values (in contrast to the RetrieveListJSON method)
  12934. function RetrieveDocVariantArray(Table: TSQLRecordClass;
  12935. const ObjectName: RawUTF8;
  12936. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  12937. const CustomFieldsCSV: RawUTF8; FirstRecordID: PID=nil;
  12938. LastRecordID: PID=nil): variant; overload;
  12939. /// get all values of a SQL statement on a single column as a TDocVariant array
  12940. // - implements REST GET collection on a single field
  12941. // - for better server speed, the WHERE clause should use bound parameters
  12942. // identified as '?' in the FormatSQLWhere statement, which is expected to
  12943. // follow the order of values supplied in BoundsSQLWhere open array - use
  12944. // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer,
  12945. // double, currency, RawUTF8 values to be bound to the request as parameters
  12946. // - the data will be converted to variants and TDocVariant following the
  12947. // TSQLRecord layout, so complex types like dynamic array would be returned
  12948. // as a true array of values (in contrast to the RetrieveListJSON method)
  12949. function RetrieveOneFieldDocVariantArray(Table: TSQLRecordClass;
  12950. const FieldName, FormatSQLWhere: RawUTF8;
  12951. const BoundsSQLWhere: array of const): variant;
  12952. /// get one member from a SQL statement as a TDocVariant
  12953. // - implements REST GET collection
  12954. // - the data will be converted to a TDocVariant variant following the
  12955. // TSQLRecord layout, so complex types like dynamic array would be returned
  12956. // as a true array of values
  12957. function RetrieveDocVariant(Table: TSQLRecordClass;
  12958. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  12959. const CustomFieldsCSV: RawUTF8): variant;
  12960. {$endif NOVARIANTS}
  12961. /// get a list of members from a SQL statement as T*ObjArray
  12962. // - implements REST GET collection
  12963. // - for better server speed, the WHERE clause should use bound parameters
  12964. // identified as '?' in the FormatSQLWhere statement, which is expected to
  12965. // follow the order of values supplied in BoundsSQLWhere open array - use
  12966. // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer,
  12967. // double, currency, RawUTF8 values to be bound to the request as parameters
  12968. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  12969. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12970. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12971. // - set the T*ObjArray variable with all items on success - so that it can
  12972. // be used with SOA methods
  12973. // - it is up to the caller to ensure that ObjClear(ObjArray) is called
  12974. // when the T*ObjArray list is not needed any more
  12975. // - returns true on success, false on error
  12976. function RetrieveListObjArray(var ObjArray; Table: TSQLRecordClass;
  12977. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  12978. const aCustomFieldsCSV: RawUTF8=''): boolean;
  12979. /// get and append a list of members as an expanded JSON array
  12980. // - implements REST GET collection
  12981. // - generates '[{rec1},{rec2},...]' using a loop similar to:
  12982. // ! while FillOne do .. AppendJsonObject() ..
  12983. // - for better server speed, the WHERE clause should use bound parameters
  12984. // identified as '?' in the FormatSQLWhere statement, which is expected to
  12985. // follow the order of values supplied in BoundsSQLWhere open array - use
  12986. // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer,
  12987. // double, currency, RawUTF8 values to be bound to the request as parameters
  12988. // - if OutputFieldName is set, the JSON array will be written as a JSON,
  12989. // property i.e. surrounded as '"OutputFieldName":[....],' - note ending ','
  12990. // - CustomFieldsCSV can be the CSV list of field names to be retrieved
  12991. // - if CustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  12992. // - if CustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  12993. // - is just a wrapper around TSQLRecord.AppendFillAsJsonArray()
  12994. procedure AppendListAsJsonArray(Table: TSQLRecordClass;
  12995. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  12996. const OutputFieldName: RawUTF8; W: TJSONSerializer;
  12997. const CustomFieldsCSV: RawUTF8='');
  12998. /// Execute directly a SQL statement, expecting a list of results
  12999. // - return a result table on success, nil on failure
  13000. // - will call EngineList() abstract method to retrieve its JSON content
  13001. function ExecuteList(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): TSQLTableJSON; virtual;
  13002. /// Execute directly a SQL statement, expecting a list of results
  13003. // - you should not have to use this method, but the ORM versions instead
  13004. // - return a result set as JSON on success, '' on failure
  13005. // - will call EngineList() abstract method to retrieve its JSON content
  13006. function ExecuteJson(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): RawJSON; virtual;
  13007. /// Execute directly a SQL statement, without any expected result
  13008. // - implements POST SQL on ModelRoot URI
  13009. // - return true on success
  13010. // - will call EngineExecute() abstract method to run the SQL statement
  13011. function Execute(const aSQL: RawUTF8): boolean; virtual;
  13012. /// Execute directly a SQL statement with supplied parameters, with no result
  13013. // - expect the same format as FormatUTF8() function, replacing all '%' chars
  13014. // with Args[] values
  13015. // - return true on success
  13016. function ExecuteFmt(const SQLFormat: RawUTF8; const Args: array of const): boolean; overload;
  13017. /// Execute directly a SQL statement with supplied parameters, with no result
  13018. // - expect the same format as FormatUTF8() function, replacing all '%' chars
  13019. // with Args[] values, and all '?' chars with Bounds[] (inlining them
  13020. // with :(...): and auto-quoting strings)
  13021. // - return true on success
  13022. function ExecuteFmt(const SQLFormat: RawUTF8; const Args, Bounds: array of const): boolean; overload;
  13023. /// unlock the corresponding record
  13024. // - record should have been locked previously e.g. with Retrieve() and
  13025. // forupdate=true, i.e. retrieved not via GET with LOCK REST-like verb
  13026. // - use our custom UNLOCK REST-like verb
  13027. // - returns true on success
  13028. function UnLock(Table: TSQLRecordClass; aID: TID): boolean; overload; virtual; abstract;
  13029. /// unlock the corresponding record
  13030. // - record should have been locked previously e.g. with Retrieve() and
  13031. // forupdate=true, i.e. retrieved not via GET with LOCK REST-like verb
  13032. // - use our custom UNLOCK REST-like method
  13033. // - calls internally UnLock() above
  13034. // - returns true on success
  13035. function UnLock(Rec: TSQLRecord): boolean; overload;
  13036. /// create a new member
  13037. // - implements REST POST collection
  13038. // - if SendData is true, client sends the current content of Value with the
  13039. // request, otherwise record is created with default values
  13040. // - if ForceID is true, client sends the Value.ID field to use this ID for
  13041. // adding the record (instead of a database-generated ID)
  13042. // - on success, returns the new ROWID value; on error, returns 0
  13043. // - on success, Value.ID is updated with the new ROWID
  13044. // - the TSQLRawBlob(BLOB) fields values are not set by this method, to
  13045. // preserve bandwidth - see UpdateBlobFields() and AddWithBlobs() methods
  13046. // - the TSQLRecordMany fields are not set either: they are separate
  13047. // instances created by TSQLRecordMany.Create, with dedicated methods to
  13048. // access to the separated pivot table
  13049. // - this method will call EngineAdd() to perform the request
  13050. function Add(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false;
  13051. DoNotAutoComputeFields: boolean=false): TID; overload;
  13052. {$ifdef HASINLINE}inline;{$endif}
  13053. /// create a new member, including selected fields
  13054. // - implements REST POST collection
  13055. // - if ForceID is true, client sends the Value.ID field to use this ID for
  13056. // adding the record (instead of a database-generated ID)
  13057. // - this method will call EngineAdd() to perform the request
  13058. function Add(Value: TSQLRecord; const CustomCSVFields: RawUTF8;
  13059. ForceID: boolean=false; DoNotAutoComputeFields: boolean=false): TID; overload;
  13060. /// create a new member, including selected fields
  13061. // - implements REST POST collection
  13062. // - if ForceID is true, client sends the Value.ID field to use this ID for
  13063. // adding the record (instead of a database-generated ID)
  13064. // - this method will call EngineAdd() to perform the request
  13065. function Add(Value: TSQLRecord; const CustomFields: TSQLFieldBits;
  13066. ForceID: boolean=false; DoNotAutoComputeFields: boolean=false): TID; overload;
  13067. {$ifdef HASINLINE}inline;{$endif}
  13068. /// create a new member, including its BLOB fields
  13069. // - implements REST POST collection
  13070. // - this method would create a JSON representation of the document
  13071. // including the BLOB fields as Base64 encoded text, so would be less
  13072. // efficient than a dual Add() + UpdateBlobFields() methods if the
  13073. // binary content has a non trivial size
  13074. // - this method will call EngineAdd() to perform the request
  13075. function AddWithBlobs(Value: TSQLRecord; ForceID: boolean=false;
  13076. DoNotAutoComputeFields: boolean=false): TID; virtual;
  13077. /// create a new member, from a supplied list of field values
  13078. // - implements REST POST collection
  13079. // - the aSimpleFields parameters must follow explicitely the order of published
  13080. // properties of the supplied aTable class, excepting the TSQLRawBlob and
  13081. // TSQLRecordMany kind (i.e. only so called "simple fields")
  13082. // - the aSimpleFields must have exactly the same count of parameters as
  13083. // there are "simple fields" in the published properties
  13084. // - if ForcedID is set to non null, client sends this ID to be used
  13085. // when adding the record (instead of a database-generated ID)
  13086. // - on success, returns the new RowID value; on error, returns 0
  13087. // - call internaly the Add virtual method above
  13088. function Add(aTable: TSQLRecordClass; const aSimpleFields: array of const;
  13089. ForcedID: TID=0): TID; overload;
  13090. /// update a member from Value simple fields content
  13091. // - implements REST PUT collection
  13092. // - return true on success
  13093. // - the TSQLRawBlob(BLOB) fields values are not updated by this method, to
  13094. // preserve bandwidth: use the UpdateBlob() methods for handling BLOB fields
  13095. // - the TSQLRecordMany fields are not set either: they are separate
  13096. // instances created by TSQLRecordMany.Create, with dedicated methods to
  13097. // access to the separated pivot table
  13098. // - if CustomFields is left void, the simple fields will be used, or the
  13099. // fields retrieved via a previous FillPrepare() call; otherwise, you can
  13100. // specify your own set of fields to be transmitted (including BLOBs, even
  13101. // if they will be Base64-encoded within the JSON content) - CustomFields
  13102. // could be computed by TSQLRecordProperties.FieldBitsFromCSV()
  13103. // or TSQLRecordProperties.FieldBitsFromRawUTF8()
  13104. // - this method will always compute and send any TModTime fields
  13105. // - this method will call EngineUpdate() to perform the request
  13106. function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[];
  13107. DoNotAutoComputeFields: boolean=false): boolean; overload; virtual;
  13108. /// update a member from Value simple fields content
  13109. // - implements REST PUT collection
  13110. // - return true on success
  13111. // - is an overloaded method to Update(Value,FieldBitsFromCSV())
  13112. function Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8;
  13113. DoNotAutoComputeFields: boolean=false): boolean; overload;
  13114. /// update a member from a supplied list of simple field values
  13115. // - implements REST PUT collection
  13116. // - the aSimpleFields parameters MUST follow explicitely both count and
  13117. // order of published properties of the supplied aTable class, excepting the
  13118. // TSQLRawBlob and TSQLRecordMany kind (i.e. only so called "simple fields")
  13119. // - return true on success
  13120. // - call internaly the Update() / EngineUpdate() virtual methods
  13121. function Update(aTable: TSQLRecordClass; aID: TID;
  13122. const aSimpleFields: array of const): boolean; overload;
  13123. /// create or update a member, depending if the Value has already an ID
  13124. // - implements REST POST if Value.ID=0 or ForceID is set, or a REST PUT
  13125. // collection to update the record pointed by a Value.ID<>0
  13126. // - will return the created or updated ID
  13127. function AddOrUpdate(Value: TSQLRecord; ForceID: boolean=false): TID;
  13128. /// update one field/column value a given member
  13129. // - implements REST PUT collection with one field value
  13130. // - only one single field shall be specified in FieldValue, but could
  13131. // be of any kind of value - for BLOBs, you should better use UpdateBlob()
  13132. // - return true on success
  13133. // - call internaly the EngineUpdateField() abstract method
  13134. // - note that this method won't update the TModTime properties: you should
  13135. // rather use a classic Retrieve()/FillPrepare() followed by Update()
  13136. function UpdateField(Table: TSQLRecordClass; ID: TID;
  13137. const FieldName: RawUTF8; const FieldValue: array of const): boolean; overload; virtual;
  13138. /// update one field in one or several members, depending on a WHERE clause
  13139. // - implements REST PUT collection with one field value on a one where value
  13140. // - only one single field shall be specified in FieldValue, but could
  13141. // be of any kind of value - for BLOBs, you should better use UpdateBlob()
  13142. // - only one single field shall be specified in WhereFieldValue, but could
  13143. // be of any kind of value - for security reasons, void WHERE clause will
  13144. // be rejected
  13145. // - return true on success
  13146. // - call internaly the EngineUpdateField() abstract method
  13147. // - note that this method won't update the TModTime properties: you should
  13148. // rather use a classic Retrieve()/FillPrepare() followed by Update()
  13149. function UpdateField(Table: TSQLRecordClass;
  13150. const WhereFieldName: RawUTF8; const WhereFieldValue: array of const;
  13151. const FieldName: RawUTF8; const FieldValue: array of const): boolean; overload; virtual;
  13152. {$ifndef NOVARIANTS}
  13153. /// update one field in a given member with a value specified as variant
  13154. // - implements REST PUT collection with one field value
  13155. // - any value can be set in FieldValue, but for BLOBs, you should better
  13156. // use UpdateBlob()
  13157. // - return true on success
  13158. // - call internaly the EngineUpdateField() abstract method
  13159. // - note that this method won't update the TModTime properties: you should
  13160. // rather use a classic Retrieve()/FillPrepare() followed by Update()
  13161. function UpdateField(Table: TSQLRecordClass; ID: TID;
  13162. const FieldName: RawUTF8; const FieldValue: variant): boolean; overload; virtual;
  13163. /// update one field in one or several members, depending on a WHERE clause,
  13164. // with both update and where values specified as variant
  13165. // - implements REST PUT collection with one field value on a one where value
  13166. // - any value can be set in FieldValue, but for BLOBs, you should better
  13167. // use UpdateBlob()
  13168. // - for security reasons, void WHERE clause will be rejected
  13169. // - return true on success
  13170. // - call internaly the EngineUpdateField() abstract method
  13171. // - note that this method won't update the TModTime properties: you should
  13172. // rather use a classic Retrieve()/FillPrepare() followed by Update()
  13173. function UpdateField(Table: TSQLRecordClass;
  13174. const WhereFieldName: RawUTF8; const WhereFieldValue: variant;
  13175. const FieldName: RawUTF8; const FieldValue: variant): boolean; overload; virtual;
  13176. /// update one field in one or several members, depending on a set of IDs
  13177. // - return true on success
  13178. // - note that this method won't update the TModTime properties: you should
  13179. // rather use a classic Retrieve()/FillPrepare() followed by Update(), but
  13180. // it would be much slower, even over a BATCH
  13181. // - will be executed as a regular SQL statement:
  13182. // $ UPDATE table SET fieldname=fieldvalue WHERE RowID IN (...)
  13183. // - warning: this method would call directly EngineExecute(), and would
  13184. // work just fine with SQLite3, but some other DB engines may not allow
  13185. // a huge number of items within the IN(...) clause
  13186. function UpdateField(Table: TSQLRecordClass; const IDs: array of Int64;
  13187. const FieldName: RawUTF8; const FieldValue: variant): boolean; overload; virtual;
  13188. {$endif NOVARIANTS}
  13189. /// increments one integer field value
  13190. // - if available, this method will use atomic value modification, e.g.
  13191. // $ UPDATE table SET field=field+?
  13192. function UpdateFieldIncrement(Table: TSQLRecordClass; ID: TID;
  13193. const FieldName: RawUTF8; Increment: Int64=1): boolean; virtual;
  13194. /// override this method to guess if this record can be updated or deleted
  13195. // - this default implementation returns always true
  13196. // - e.g. you can add digital signature to a record to disallow record editing
  13197. // - the ErrorMsg can be set to a variable, which will contain an explicit
  13198. // error message
  13199. function RecordCanBeUpdated(Table: TSQLRecordClass; ID: TID; Action: TSQLEvent;
  13200. ErrorMsg: PRawUTF8 = nil): boolean; virtual;
  13201. /// delete a member
  13202. // - implements REST DELETE collection
  13203. // - return true on success
  13204. // - call internaly the EngineDelete() abstract method
  13205. function Delete(Table: TSQLRecordClass; ID: TID): boolean; overload; virtual;
  13206. /// delete a member with a WHERE clause
  13207. // - implements REST DELETE collection
  13208. // - return true on success
  13209. // - this default method call OneFieldValues() to retrieve all matching IDs,
  13210. // then will delete each row using protected EngineDeleteWhere() virtual method
  13211. function Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean; overload; virtual;
  13212. /// delete a member with a WHERE clause
  13213. // - implements REST DELETE collection
  13214. // - return true on success
  13215. // - for better server speed, the WHERE clause should use bound parameters
  13216. // identified as '?' in the FormatSQLWhere statement, which is expected to
  13217. // follow the order of values supplied in BoundsSQLWhere open array - use
  13218. // DateToSQL/DateTimeToSQL for TDateTime, or directly any integer / double /
  13219. // currency / RawUTF8 values to be bound to the request as parameters
  13220. // - is a simple wrapper around:
  13221. // ! Delete(Table,FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere))
  13222. function Delete(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8;
  13223. const BoundsSQLWhere: array of const): boolean; overload;
  13224. /// access the internal caching parameters for a given TSQLRecord
  13225. // - will always return a TSQLRestCache instance, creating one if needed
  13226. // - purpose of this caching mechanism is to speed up retrieval of some
  13227. // common values at either Client or Server level (like configuration settings)
  13228. // - by default, this CRUD level per-ID cache is disabled
  13229. // - use Cache.SetCache() and Cache.SetTimeOut() methods to set the appropriate
  13230. // configuration for this particular TSQLRest instance
  13231. // - only caching synchronization is about the direct RESTful/CRUD commands:
  13232. // RETRIEVE, ADD, UPDATE and DELETE (that is, a complex direct SQL UPDATE or
  13233. // via TSQLRecordMany pattern won't be taken in account - only exception is
  13234. // TSQLRestStorage tables accessed as SQLite3 virtual table)
  13235. // - this caching will be located at the TSQLRest level, that is no automated
  13236. // synchronization is implemented between TSQLRestClient and TSQLRestServer -
  13237. // you shall ensure that your business logic is safe, calling Cache.Flush()
  13238. // overloaded methods on purpose: better no cache than unproper cache -
  13239. // "premature optimization is the root of all evil"
  13240. property Cache: TSQLRestCache read GetCache;
  13241. /// access the internal caching parameters for a given TSQLRecord
  13242. // - would return nil if no TSQLRestCache instance has been defined
  13243. function CacheOrNil: TSQLRestCache;
  13244. {$ifdef HASINLINE}inline;{$endif}
  13245. /// get a blob field content from its record ID and supplied blob field name
  13246. // - implements REST GET collection with a supplied member ID and a blob field name
  13247. // - return true on success
  13248. // - this method is defined as abstract, i.e. there is no default implementation:
  13249. // it must be implemented 100% RestFul with a
  13250. // GET ModelRoot/TableName/TableID/BlobFieldName request for example
  13251. // - this method retrieve the blob data as a TSQLRawBlob string using
  13252. // EngineRetrieveBlob()
  13253. function RetrieveBlob(Table: TSQLRecordClass; aID: TID;
  13254. const BlobFieldName: RawUTF8; out BlobData: TSQLRawBlob): boolean; overload; virtual;
  13255. /// get a blob field content from its record ID and supplied blob field name
  13256. // - implements REST GET collection with a supplied member ID and field name
  13257. // - return true on success
  13258. // - this method will create a TStream instance (which must be freed by the
  13259. // caller after use) and fill it with the blob data
  13260. function RetrieveBlob(Table: TSQLRecordClass; aID: TID;
  13261. const BlobFieldName: RawUTF8; out BlobStream: THeapMemoryStream): boolean; overload;
  13262. /// update a blob field from its record ID and supplied blob field name
  13263. // - implements REST PUT collection with a supplied member ID and field name
  13264. // - return true on success
  13265. // - call internaly the EngineUpdateBlob() abstract method
  13266. // - this method expect the Blob data to be supplied as TSQLRawBlob, using
  13267. // EngineUpdateBlob()
  13268. function UpdateBlob(Table: TSQLRecordClass; aID: TID;
  13269. const BlobFieldName: RawUTF8; const BlobData: TSQLRawBlob): boolean; overload; virtual;
  13270. /// update a blob field from its record ID and blob field name
  13271. // - implements REST PUT collection with a supplied member ID and field name
  13272. // - return true on success
  13273. // - call internaly the EngineUpdateBlob() abstract method
  13274. // - this method expect the Blob data to be supplied as a TStream: it will
  13275. // send the whole stream content (from its beginning position upto its
  13276. // current size) to the database engine
  13277. function UpdateBlob(Table: TSQLRecordClass; aID: TID;
  13278. const BlobFieldName: RawUTF8; BlobData: TStream): boolean; overload;
  13279. /// update a blob field from its record ID and blob field name
  13280. // - implements REST PUT collection with a supplied member ID and field name
  13281. // - return true on success
  13282. // - call internaly the EngineUpdateBlob() abstract method
  13283. // - this method expect the Blob data to be supplied as direct memory pointer
  13284. // and size
  13285. function UpdateBlob(Table: TSQLRecordClass; aID: TID;
  13286. const BlobFieldName: RawUTF8; BlobData: pointer; BlobSize: integer): boolean; overload;
  13287. /// update all BLOB fields of the supplied Value
  13288. // - call several REST PUT collection (one for each BLOB) for the member
  13289. // - uses the UpdateBlob() method to send the BLOB properties content to the Server
  13290. // - called internaly by Add and Update methods when ForceBlobTransfert /
  13291. // ForceBlobTransfertTable[] is set
  13292. // - you can use this method by hand, to avoid several calls to UpdateBlob()
  13293. // - returns TRUE on success (or if there is no BLOB field)
  13294. // - returns FALSE on error (e.g. if Value is invalid or with db/transmission)
  13295. function UpdateBlobFields(Value: TSQLRecord): boolean; virtual;
  13296. /// get all BLOB fields of the supplied value from the remote server
  13297. // - call several REST GET collection (one for each BLOB) for the member
  13298. // - call internaly e.g. by TSQLRestClient.Retrieve method when
  13299. // ForceBlobTransfert / ForceBlobTransfertTable[] is set
  13300. function RetrieveBlobFields(Value: TSQLRecord): boolean; virtual;
  13301. /// begin a transaction
  13302. // - implements REST BEGIN collection
  13303. // - may be used to speed up CRUD statements like Add/Update/Delete
  13304. // - in the current implementation, nested transactions are not allowed
  13305. // - must be ended with Commit on success
  13306. // - must be aborted with Rollback if any SQL statement failed
  13307. // - default implementation just handle the protected fTransactionActiveSession flag
  13308. // - return true if no transaction is active, false otherwise
  13309. // - in aClient-Server environment with multiple Clients connected at the
  13310. // same time, you should better use BATCH process, specifying a positive
  13311. // AutomaticTransactionPerRow parameter to BatchStart()
  13312. // - in a multi-threaded or Client-Server with multiple concurrent Client
  13313. // connections, you may check the returned value, as such:
  13314. // !if Client.TransactionBegin(TSQLRecordPeopleObject) then
  13315. // !try
  13316. // ! //.... modify the database content, raise exceptions on error
  13317. // ! Client.Commit;
  13318. // !except
  13319. // ! Client.RollBack; // in case of error
  13320. // !end;
  13321. // or use the TransactionBeginRetry() method
  13322. // - the supplied SessionID will allow multi-user transaction safety on the
  13323. // Server-Side: all database modification from another session will wait
  13324. // for the global transaction to be finished; on Client-side, the SessionID
  13325. // is just ignored (TSQLRestClient will override this method with a default
  13326. // SessionID=CONST_AUTHENTICATION_NOT_USED=1 parameter)
  13327. // - if you have an external database engine which expect transactions to
  13328. // take place in the same thread, ensure TSQLRestServer force execution of
  13329. // this method when accessed from RESTful clients in the same thread, e.g.:
  13330. // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread;
  13331. // ! AcquireWriteMode := amBackgroundThread; // same as previous
  13332. function TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal): boolean; virtual;
  13333. /// check current transaction status
  13334. // - returns the session ID if a transaction is active
  13335. // - returns 0 if no transaction is active
  13336. function TransactionActiveSession: cardinal;
  13337. /// end a transaction
  13338. // - implements REST END collection
  13339. // - write all pending SQL statements to the disk
  13340. // - default implementation just reset the protected fTransactionActiveSession flag
  13341. // - the supplied SessionID will allow multi-user transaction safety on the
  13342. // Server-Side: all database modification from another session will wait
  13343. // for the global transaction to be finished; on Client-side, the SessionID
  13344. // is just ignored (TSQLRestClient will override this method with a default
  13345. // SessionID=CONST_AUTHENTICATION_NOT_USED=1 parameter)
  13346. // - if you have an external database engine which expect transactions to
  13347. // take place in the same thread, ensure TSQLRestServer force execution of
  13348. // this method when accessed from RESTful clients in the same thread, e.g.:
  13349. // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread;
  13350. // ! AcquireWriteMode := amBackgroundThread; // same as previous
  13351. // - by default, any exception will be catch and ignored, unless RaiseException
  13352. // is set to TRUE so that the caller would be able to handle it
  13353. procedure Commit(SessionID: cardinal; RaiseException: boolean=false); virtual;
  13354. /// abort a transaction
  13355. // - implements REST ABORT collection
  13356. // - restore the previous state of the database, before the call to TransactionBegin
  13357. // - default implementation just reset the protected fTransactionActiveSession flag
  13358. // - the supplied SessionID will allow multi-user transaction safety on the
  13359. // Server-Side: all database modification from another session will wait
  13360. // for the global transaction to be finished; on Client-side, the SessionID
  13361. // is just ignored (TSQLRestClient will override this method with a default
  13362. // SessionID=CONST_AUTHENTICATION_NOT_USED=1 parameter)
  13363. // - if you have an external database engine which expect transactions to
  13364. // take place in the same thread, ensure TSQLRestServer force execution of
  13365. // this method when accessed from RESTful clients in the same thread, e.g.:
  13366. // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread;
  13367. // ! AcquireWriteMode := amBackgroundThread; // same as previous
  13368. procedure RollBack(SessionID: cardinal); virtual;
  13369. /// execute a BATCH sequence prepared in a TSQLRestBatch instance
  13370. // - implements the "Unit Of Work" pattern, i.e. safe transactional process
  13371. // even on multi-thread environments
  13372. // - send all pending Add/Update/Delete statements to the DB or remote server
  13373. // - will return the URI Status value, i.e. 200/HTML_SUCCESS OK on success
  13374. // - a dynamic array of integers will be created in Results,
  13375. // containing all ROWDID created for each BatchAdd call, 200 (=HTML_SUCCESS)
  13376. // for all successfull BatchUpdate/BatchDelete, or 0 on error
  13377. // - any error during server-side process MUST be checked against Results[]
  13378. // (the main URI Status is 200 if about communication success, and won't
  13379. // imply that all statements in the BATCH sequence were successfull
  13380. // - note that the caller shall still free the supplied Batch instance
  13381. function BatchSend(Batch: TSQLRestBatch; var Results: TIDDynArray): integer; overload; virtual;
  13382. /// execute a BATCH sequence prepared in a TSQLRestBatch instance
  13383. // - just a wrapper around the overloaded BatchSend() method without the
  13384. // Results: TIDDynArray parameter
  13385. function BatchSend(Batch: TSQLRestBatch): integer; overload;
  13386. {$ifdef ISDELPHI2010} // Delphi 2009 generics support is buggy :(
  13387. /// get an instance of one interface-based service
  13388. // - may return nil if this service interface is not available
  13389. function Service<T: IInterface>: T;
  13390. /// get a list of members from a SQL statement
  13391. // - implements REST GET collection
  13392. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  13393. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  13394. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  13395. // - return a TObjectList<T> on success (possibly with Count=0) - caller is
  13396. // responsible of freeing the instance
  13397. // - return nil on error
  13398. // - you can write for instance:
  13399. // !var List: TObjectList<TSQLRecordTest>;
  13400. // ! R: TSQLRecordTest;
  13401. // ! ...
  13402. // ! List := Client.RetrieveList<TSQLRecordTest>('ID,Test');
  13403. // ! if List<>nil then
  13404. // ! try
  13405. // ! for R in List do
  13406. // ! writeln(R.ID,'=',R.Test);
  13407. // ! finally
  13408. // ! List.Free;
  13409. // ! end;
  13410. function RetrieveList<T: TSQLRecord>(const aCustomFieldsCSV: RawUTF8=''): TObjectList<T>; overload;
  13411. {$ifdef HASINLINE}inline;{$endif}
  13412. /// get a list of members from a SQL statement
  13413. // - implements REST GET collection with a WHERE clause
  13414. // - for better server speed, the WHERE clause should use bound parameters
  13415. // identified as '?' in the FormatSQLWhere statement, which is expected to
  13416. // follow the order of values supplied in BoundsSQLWhere open array - use
  13417. // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer,
  13418. // double, currency, RawUTF8 values to be bound to the request as parameters
  13419. // - aCustomFieldsCSV can be the CSV list of field names to be retrieved
  13420. // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs
  13421. // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs
  13422. // - return a TObjectList<T> on success (possibly with Count=0) - caller is
  13423. // responsible of freeing the instance
  13424. // - return nil on error
  13425. function RetrieveList<T: TSQLRecord>(const FormatSQLWhere: RawUTF8;
  13426. const BoundsSQLWhere: array of const;
  13427. const aCustomFieldsCSV: RawUTF8=''): TObjectList<T>; overload;
  13428. {$endif}
  13429. /// you can call this method in TThread.Execute to ensure that
  13430. // the thread will be taken in account during process
  13431. // - this abstract method won't do anything, but TSQLRestServer's will
  13432. procedure BeginCurrentThread(Sender: TThread); virtual;
  13433. /// you can call this method just before a thread is finished to ensure
  13434. // e.g. that the associated external DB connection will be released
  13435. // - this abstract method will call fLogClass.Add.NotifyThreadEnded
  13436. // but TSQLRestServer.EndCurrentThread would do the main process
  13437. procedure EndCurrentThread(Sender: TThread); virtual;
  13438. /// allows to safely execute a processing method in a background thread
  13439. // - returns a TSynBackgroundThreadMethod instance, ready to execute any
  13440. // background task via its RunAndWait() method
  13441. // - will properly call BeginCurrentThread/EndCurrentThread methods
  13442. // - you should supply some runtime information to name the thread, for
  13443. // proper debugging
  13444. function NewBackgroundThreadMethod(const Format: RawUTF8; const Args: array of const): TSynBackgroundThreadMethod;
  13445. /// allows to safely execute a process at a given pace
  13446. // - returns a TSynBackgroundThreadProcess instance, ready to execute the
  13447. // supplied aOnProcess event in a loop, as aOnProcessMS periodic task
  13448. // - will properly call BeginCurrentThread/EndCurrentThread methods
  13449. // - you should supply some runtime information to name the thread, for
  13450. // proper debugging
  13451. function NewBackgroundThreadProcess(aOnProcess: TOnSynBackgroundThreadProcess;
  13452. aOnProcessMS: cardinal; const Format: RawUTF8; const Args: array of const;
  13453. aStats: TSynMonitorClass=nil): TSynBackgroundThreadProcess;
  13454. /// how this class execute its internal commands
  13455. // - by default, TSQLRestServer.URI() will lock for Write ORM according to
  13456. // AcquireWriteMode (i.e. AcquireExecutionMode[execORMWrite]=amLocked) and
  13457. // other operations won't be protected (for better scaling)
  13458. // - you can tune this behavior by setting this property to the expected
  13459. // execution mode, e.g. execute all method-based services in a dedicated
  13460. // thread via
  13461. // ! aServer.AcquireExecutionMode[execSOAByMethod] := amBackgroundThread;
  13462. // - if you use external DB and a custom ConnectionTimeOutMinutes value,
  13463. // both read and write access should be locked, so you should set:
  13464. // ! aServer.AcquireExecutionMode[execORMGet] := am***;
  13465. // ! aServer.AcquireExecutionMode[execORMWrite] := am***;
  13466. // here, safe blocking am*** modes are any mode but amUnlocked, i.e. either
  13467. // amLocked, amBackgroundThread, amBackgroundORMSharedThread or amMainThread
  13468. property AcquireExecutionMode[Cmd: TSQLRestServerURIContextCommand]: TSQLRestServerAcquireMode
  13469. read GetAcquireExecutionMode write SetAcquireExecutionMode;
  13470. /// the time (in mili seconds) to try locking internal commands of this class
  13471. // - this value is used only for AcquireExecutionMode[*]=amLocked
  13472. // - by default, TSQLRestServer.URI() will lock for Write ORM according to
  13473. // AcquireWriteTimeOut (i.e. AcquireExecutionLockedTimeOut[execORMWrite])
  13474. // and other operations won't be locked nor have any time out set
  13475. property AcquireExecutionLockedTimeOut[Cmd: TSQLRestServerURIContextCommand]: cardinal
  13476. read GetAcquireExecutionLockedTimeOut write SetAcquireExecutionLockedTimeOut;
  13477. /// how this class will handle write access to the database
  13478. // - is a common wrapper to AcquireExecutionMode[execORMWrite] property
  13479. // - default amLocked mode will wait up to AcquireWriteTimeOut mili seconds
  13480. // to have a single access to the server write ORM methods
  13481. // - amBackgroundThread will execute the write methods in a queue, in a
  13482. // dedicated unique thread (which can be convenient, especially for
  13483. // external database transaction process)
  13484. // - amBackgroundORMSharedThread will execute all ORM methods in a queue, in
  13485. // a dedicated unique thread, shared for both execORMWrite and execORMGet,
  13486. // but still dedicated for execSOAByMethod and execSOAByInterface
  13487. // - a slower alternative to amBackgroundThread may be amMainThread
  13488. // - you can set amUnlocked for a concurrent write access, but be aware
  13489. // that it may lead into multi-thread race condition issues, depending on
  13490. // the database engine used
  13491. property AcquireWriteMode: TSQLRestServerAcquireMode index execORMWrite
  13492. read GetAcquireExecutionMode write SetAcquireExecutionMode;
  13493. /// the time (in mili seconds) which the class will wait for acquiring a
  13494. // write acccess to the database, when AcquireWriteMode is amLocked
  13495. // - is a common wrapper to AcquireExecutionLockedTimeOut[execORMWrite]
  13496. // - in order to handle safe transactions and multi-thread safe writing, the
  13497. // server will identify transactions using the client Session ID: this
  13498. // property will set the time out wait period
  13499. // - default value is 2000, i.e. TSQLRestServer.URI will wait up to 2 seconds
  13500. // in order to acquire the right to write on the database before returning
  13501. // a "408 Request Time-out" status error
  13502. property AcquireWriteTimeOut: cardinal index execORMWrite
  13503. read GetAcquireExecutionLockedTimeOut write SetAcquireExecutionLockedTimeOut;
  13504. /// used e.g. by IAdministratedDaemon to implement "pseudo-SQL" commands
  13505. // - this default implementation will handle #time #model #rest commands
  13506. procedure AdministrationExecute(const DatabaseName,SQL: RawUTF8;
  13507. var result: TServiceCustomAnswer); virtual;
  13508. /// access to the interface-based services list
  13509. // - may be nil if no service interface has been registered yet: so be
  13510. // aware that the following line may trigger an access violation if
  13511. // no ICalculator is defined on server side:
  13512. // ! if fServer.Services['Calculator'].Get(Calc)) then
  13513. // ! ...
  13514. // - safer typical use, following the DI/IoC pattern, and which would not
  13515. // trigger any access violation if Services=nil, could be:
  13516. // ! if fServer.Services.Resolve(ICalculator,Calc) then
  13517. // ! ...
  13518. property Services: TServiceContainer read fServices;
  13519. /// access or initialize the internal IoC resolver, used for interface-based
  13520. // remote services, and more generaly any Services.Resolve() call
  13521. // - create and initialize the internal TServiceContainer if no service
  13522. // interface has been registered yet // - may be used to inject some dependencies, which are not interface-based
  13523. // remote services, but internal IoC, without the ServiceRegister()
  13524. // or ServiceDefine() methods - e.g.
  13525. // ! aRest.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(aRest)],true);
  13526. // - overriden methods would return TServiceContainerClient or
  13527. // TServiceContainerServer instances, on TSQLRestClient or TSQLRestServer
  13528. function ServiceContainer: TServiceContainer; virtual; abstract;
  13529. /// the routing classs of the service remote request
  13530. // - by default, will use TSQLRestRoutingREST, i.e. an URI-based
  13531. // layout which is secure (since will use our RESTful authentication scheme),
  13532. // and also very fast
  13533. // - but TSQLRestRoutingJSON_RPC can e.g. be set (on BOTH client and
  13534. // server sides), if the client would rather use JSON/RPC alternative pattern
  13535. // - NEVER set the abstract TSQLRestServerURIContext class on this property
  13536. property ServicesRouting: TSQLRestServerURIContextClass
  13537. read fRoutingClass write SetRoutingClass;
  13538. /// the Database Model associated with this REST Client or Server
  13539. property Model: TSQLModel read fModel;
  13540. published
  13541. /// the current UTC Date and Time, as retrieved from the server
  13542. // - this property will return the timestamp as TTimeLog / Int64
  13543. // after correction from the Server returned time-stamp (if any)
  13544. // - is used e.g. by TSQLRecord.ComputeFieldsBeforeWrite to update TModTime
  13545. // and TCreateTime published fields
  13546. // - default implementation will return the executable UTC time, i.e. NowUTC
  13547. // so that any GUI code should convert this UTC value into local time
  13548. // - on TSQLRestServer, if you use an external database, the TSQLDBConnection
  13549. // ServerTimeStamp value will be set to this property
  13550. // - you can use this value in a WHERE clause for a query, as such:
  13551. // ! aRec.CreateAndFillPrepare(Client,'Datum<=?',[TimeLogToSQL(Client.ServerTimeStamp)]);
  13552. // - or you could use ServerTimeStamp everywhere in your code, when you need
  13553. // a reference time base
  13554. property ServerTimeStamp: TTimeLog read GetServerTimeStamp write SetServerTimeStamp;
  13555. {$ifdef WITHLOG}
  13556. /// the logging class used for this instance
  13557. // - is set by default to SQLite3Log, but could be set to a custom class
  13558. property LogClass: TSynLogClass read GetLogClass write SetLogClass;
  13559. {$endif}
  13560. public
  13561. /// the custom queries parameters for User Interface Query action
  13562. QueryCustom: array of TSQLQueryCustom;
  13563. /// evaluate a basic operation for implementing User Interface Query action
  13564. // - expect both Value and Reference to be UTF-8 encoded (as in TSQLTable
  13565. // or TSQLTableToGrid)
  13566. // - aID parameter is ignored in this function implementation (expect only
  13567. // this parameter to be not equal to 0)
  13568. // - is TSQLQueryEvent prototype compatible
  13569. // - for qoContains and qoBeginWith, the Reference is expected to be
  13570. // already uppercase
  13571. // - for qoSoundsLike* operators, Reference is not a PUTF8Char, but a
  13572. // typecase of a prepared TSynSoundEx object instance (i.e. pointer(@SoundEx))
  13573. class function QueryIsTrue(aTable: TSQLRecordClass; aID: TID;
  13574. FieldType: TSQLFieldType; Value: PUTF8Char; Operator: integer;
  13575. Reference: PUTF8Char): boolean;
  13576. /// add a custom query
  13577. // - one event handler with an enumeration type containing all available
  13578. // query names
  13579. // - and associated operators
  13580. procedure QueryAddCustom(aTypeInfo: pointer; aEvent: TSQLQueryEvent;
  13581. const aOperators: TSQLQueryOperators);
  13582. end;
  13583. {$M+}
  13584. /// a simple TThread for doing some process within the context of a REST instance
  13585. // - also define a Start method for compatibility with older versions of Delphi
  13586. // - inherited classes should override InternalExecute abstract method
  13587. TSQLRestThread = class(TThread)
  13588. protected
  13589. fRest: TSQLRest;
  13590. fOwnRest: boolean;
  13591. fLog: TSynLog;
  13592. fSafe: TSynLocker;
  13593. /// will call BeginCurrentThread/EndCurrentThread and catch exceptions
  13594. procedure Execute; override;
  13595. /// you should override this method with the proper process
  13596. procedure InternalExecute; virtual; abstract;
  13597. public
  13598. /// initialize the thread
  13599. // - if aOwnRest is TRUE, the supplied REST instance would be
  13600. // owned by this thread
  13601. constructor Create(aRest: TSQLRest; aOwnRest, aCreateSuspended: boolean);
  13602. {$ifndef HASTTHREADSTART}
  13603. /// method to be called to start the thread
  13604. // - Resume is deprecated in the newest RTL, since some OS - e.g. Linux -
  13605. // do not implement this pause/resume feature; we define here this method
  13606. // for older versions of Delphi
  13607. procedure Start;
  13608. {$endif}
  13609. /// finalize the thread
  13610. // - and the associated REST instance if OwnRest is TRUE
  13611. destructor Destroy; override;
  13612. /// safe version of Sleep() which won't break the thread process
  13613. // - returns TRUE if the thread was Terminated
  13614. // - returns FALSE if successfully waited up to MS milliseconds
  13615. function SleepOrTerminated(MS: integer): boolean;
  13616. /// read-only access to the associated REST instance
  13617. property Rest: TSQLRest read FRest;
  13618. /// TRUE if the associated REST instance would be owned by this thread
  13619. property OwnRest: boolean read fOwnRest;
  13620. /// a critical section is associated to this thread
  13621. // - could be used to protect shared resources within the internal process
  13622. property Safe: TSynLocker read fSafe;
  13623. /// read-only access to the TSynLog instance of the associated REST instance
  13624. property Log: TSynLog read fLog;
  13625. /// publishes the thread running state
  13626. property Terminated;
  13627. end;
  13628. {$M-}
  13629. /// event signature used to notify a client callback
  13630. // - implemented e.g. by TSQLHttpServer.NotifyCallback
  13631. TSQLRestServerNotifyCallback = function(aSender: TSQLRestServer;
  13632. const aInterfaceDotMethodName,aParams: RawUTF8;
  13633. aConnectionID: Int64; aFakeCallID: integer;
  13634. aResult, aErrorMsg: PRawUTF8): boolean of object;
  13635. /// event signature used by TSQLRestServer.OnServiceCreateInstance
  13636. // - as called by TServiceFactoryServer.CreateInstance
  13637. // - the actual Instance class can be quickly retrieved from
  13638. // Sender.ImplementationClass
  13639. TOnServiceCreateInstance = procedure(
  13640. Sender: TServiceFactoryServer; Instance: TInterfacedObject) of object;
  13641. {$ifdef MSWINDOWS}
  13642. /// Server thread accepting connections from named pipes
  13643. TSQLRestServerNamedPipe = class(TSQLRestThread)
  13644. private
  13645. protected
  13646. fServer: TSQLRestServer;
  13647. fChild: TList;
  13648. fChildCount: integer;
  13649. fPipeName: TFileName;
  13650. procedure InternalExecute; override;
  13651. public
  13652. /// create the server thread
  13653. constructor Create(aServer: TSQLRestServer; const PipeName: TFileName); reintroduce;
  13654. /// release all associated memory, and wait for all
  13655. // TSQLRestServerNamedPipeResponse children to be terminated
  13656. destructor Destroy; override;
  13657. /// the associated pipe name
  13658. property PipeName: TFileName read fPipeName;
  13659. end;
  13660. /// Server child thread dealing with a connection through a named pipe
  13661. TSQLRestServerNamedPipeResponse = class(TSQLRestThread)
  13662. private
  13663. protected
  13664. fServer: TSQLRestServer;
  13665. fPipe: cardinal;
  13666. fMasterThread: TSQLRestServerNamedPipe;
  13667. fMasterThreadChildIndex: Integer;
  13668. procedure InternalExecute; override;
  13669. public
  13670. /// create the child connection thread
  13671. constructor Create(aServer: TSQLRestServer; aMasterThread: TSQLRestServerNamedPipe;
  13672. aPipe: cardinal); reintroduce;
  13673. /// release all associated memory, and decrement fMasterThread.fChildCount
  13674. destructor Destroy; override;
  13675. end;
  13676. {$ifdef FPC}
  13677. TWMCopyData = record
  13678. Msg: UINT;
  13679. From: WPARAM;
  13680. CopyDataStruct: LPARAM;
  13681. Result: LRESULT;
  13682. end;
  13683. {$endif}
  13684. {$endif}
  13685. /// function prototype for remotely calling a TSQLRestServer
  13686. // - use PUTF8Char instead of string: no need to share a memory manager, and can
  13687. // be used with any language (even C or .NET, thanks to the cdecl calling convention)
  13688. // - you can specify some POST/PUT data in SendData (leave as nil otherwise)
  13689. // - returns in result.Lo the HTTP STATUS integer error or success code
  13690. // - returns in result.Hi the server database internal status
  13691. // - on success, allocate and store the resulting JSON body into Resp^, headers in Head^
  13692. // - use a GlobalFree() function to release memory for Resp and Head responses
  13693. TURIMapRequest = function(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl;
  13694. TSQLRestServerAuthentication = class;
  13695. /// structure used to specify custom request paging parameters for TSQLRestServer
  13696. // - default values are the one used for YUI component paging (i.e.
  13697. // PAGINGPARAMETERS_YAHOO constant, as set by TSQLRestServer.Create)
  13698. // - warning: using paging can be VERY expensive on Server side, especially
  13699. // when used with external databases (since all data is retrieved before
  13700. // paging, when SQLite3 works in virtual mode)
  13701. TSQLRestServerURIPagingParameters = record
  13702. /// parameter name used to specify the request sort order
  13703. // - default value is 'SORT='
  13704. Sort: PAnsiChar;
  13705. /// parameter name used to specify the request sort direction
  13706. // - default value is 'DIR='
  13707. Dir: PAnsiChar;
  13708. /// parameter name used to specify the request starting offset
  13709. // - default value is 'STARTINDEX='
  13710. StartIndex: PAnsiChar;
  13711. /// parameter name used to specify the request the page size (LIMIT clause)
  13712. // - default value is 'RESULTS='
  13713. Results: PAnsiChar;
  13714. /// parameter name used to specify the request field names
  13715. // - default value is 'SELECT='
  13716. Select: PAnsiChar;
  13717. /// parameter name used to specify the request WHERE clause
  13718. // - default value is 'WHERE='
  13719. Where: PAnsiChar;
  13720. /// returned JSON field value of optional total row counts
  13721. // - default value is nil, i.e. no total row counts field
  13722. // - computing total row counts can be very expensive, depending on the
  13723. // database back-end used (especially for external databases)
  13724. // - can be set e.g. to ',"totalRows":%' value (note that the initial "," is
  13725. // expected by the produced JSON content, and % will be set with the value)
  13726. SendTotalRowsCountFmt: RawUTF8;
  13727. end;
  13728. /// used to define how to trigger Events on record update
  13729. // - see TSQLRestServer.OnUpdateEvent property and InternalUpdateEvent() method
  13730. // - returns true on success, false if an error occured (but action must continue)
  13731. // - to be used only server-side, not to synchronize some clients: the framework
  13732. // is designed around a stateless RESTful architecture (like HTTP/1.1), in which
  13733. // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer)
  13734. TNotifySQLEvent = function(Sender: TSQLRestServer; Event: TSQLEvent;
  13735. aTable: TSQLRecordClass; const aID: TID; const aSentData: RawUTF8): boolean of object;
  13736. /// used to define how to trigger Events on record field update
  13737. // - see TSQLRestServer.OnBlobUpdateEvent property and InternalUpdateEvent() method
  13738. // - returns true on success, false if an error occured (but action must continue)
  13739. // - to be used only server-side, not to synchronize some clients: the framework
  13740. // is designed around a stateless RESTful architecture (like HTTP/1.1), in which
  13741. // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer)
  13742. TNotifyFieldSQLEvent = function(Sender: TSQLRestServer; Event: TSQLEvent;
  13743. aTable: TSQLRecordClass; const aID: TID; const aAffectedFields: TSQLFieldBits): boolean of object;
  13744. /// session-related callbacks triggered by TSQLRestServer
  13745. // - for OnSessionCreate, returning TRUE will abort the session creation -
  13746. // and you can set Ctxt.Call^.OutStatus to a corresponding error code
  13747. TNotifySQLSession = function(Sender: TSQLRestServer; Session: TAuthSession;
  13748. Ctxt: TSQLRestServerURIContext): boolean of object;
  13749. /// callback allowing to customize the retrieval of an authenticated user
  13750. // - as defined in TSQLRestServer.OnAuthenticationUserRetrieve
  13751. // - and executed by TSQLRestServerAuthentication.GetUser
  13752. // - on call, either aUserID would be <> 0, or aUserName is to be used
  13753. // - if the function returns nil, default Server.SQLAuthUserClass.Create()
  13754. // methods won't be called, and the user will be reported as not found
  13755. TOnAuthenticationUserRetrieve = function(Sender: TSQLRestServerAuthentication;
  13756. Ctxt: TSQLRestServerURIContext; aUserID: TID; const aUserName: RawUTF8): TSQLAuthUser of object;
  13757. /// callback raised in case of authentication failure
  13758. // - as used by TSQLRestServerURIContext.AuthenticationFailed event
  13759. TNotifyAuthenticationFailed = procedure(Sender: TSQLRestServer;
  13760. Reason: TNotifyAuthenticationFailedReason; Session: TAuthSession;
  13761. Ctxt: TSQLRestServerURIContext) of object;
  13762. /// callback raised before TSQLRestServer.URI execution
  13763. // - should return TRUE to execute the command, FALSE to cancel it
  13764. TNotifyBeforeURI = function(Ctxt: TSQLRestServerURIContext): boolean of object;
  13765. /// callback raised after TSQLRestServer.URI execution
  13766. TNotifyAfterURI = procedure(Ctxt: TSQLRestServerURIContext) of object;
  13767. /// callback raised if TSQLRestServer.URI execution failed
  13768. // - should return TRUE to execute Ctxt.Error(E,...), FALSE if returned
  13769. // content has already been set as expected by the client
  13770. TNotifyErrorURI = function(Ctxt: TSQLRestServerURIContext; E: Exception): boolean of object;
  13771. TSQLRestStorageInMemory = class;
  13772. TSQLVirtualTableModule = class;
  13773. /// class-reference type (metaclass) of our abstract table storage
  13774. // - may be e.g. TSQLRestStorageInMemory, TSQLRestStorageInMemoryExternal,
  13775. // TSQLRestStorageExternal or TSQLRestStorageMongoDB
  13776. TSQLRestStorageClass = class of TSQLRestStorage;
  13777. /// class-reference type (metaclass) of our TObjectList memory-stored table storage
  13778. // - may be TSQLRestStorageInMemory or TSQLRestStorageInMemoryExternal
  13779. TSQLRestStorageInMemoryClass = class of TSQLRestStorageInMemory;
  13780. /// table containing the available user access rights for authentication
  13781. // - this class should be added to the TSQLModel, together with TSQLAuthUser,
  13782. // to allow authentication support
  13783. // - you can inherit from it to add your custom properties to each user info:
  13784. // TSQLModel will search for any class inheriting from TSQLAuthGroup to
  13785. // manage per-group authorization data
  13786. // - by default, it won't be accessible remotely by anyone
  13787. TSQLAuthGroup = class(TSQLRecord)
  13788. private
  13789. fIdent: RawUTF8;
  13790. fSessionTimeOut: integer;
  13791. fAccessRights: RawUTF8;
  13792. function GetSQLAccessRights: TSQLAccessRights;
  13793. procedure SetSQLAccessRights(const Value: TSQLAccessRights);
  13794. public
  13795. /// called when the associated table is created in the database
  13796. // - on a new database, if TSQLAuthUser and TSQLAuthGroup tables are defined
  13797. // in the associated TSQLModel, it this will add 'Admin', 'Supervisor',
  13798. // and 'User' rows in the AuthUser table (with 'synopse' as default password),
  13799. // and associated 'Admin', 'Supervisor', 'User' and 'Guest' groups, with the
  13800. // following access rights to the AuthGroup table:
  13801. // $ POSTSQL SELECTSQL Service AuthR AuthW TablesR TablesW
  13802. // $ Admin Yes Yes Yes Yes Yes Yes Yes
  13803. // $ Supervisor No Yes Yes Yes No Yes Yes
  13804. // $ User No No Yes No No Yes Yes
  13805. // $ Guest No No No No No Yes No
  13806. // - 'Admin' will be the only able to execute remote not SELECT SQL statements
  13807. // for POST commands (reSQL flag in TSQLAccessRights.AllowRemoteExecute) and
  13808. // modify the Auth tables (i.e. AuthUser and AuthGroup)
  13809. // - 'Admin' and 'Supervisor' will allow any SELECT SQL statements to be
  13810. // executed, even if the table can't be retrieved and checked (corresponding
  13811. // to the reSQLSelectWithoutTable flag)
  13812. // - 'User' won't have the reSQLSelectWithoutTable flag, nor the right
  13813. // to retrieve the Auth tables data for other users
  13814. // - 'Guest' won't have access to the interface-based remote JSON-RPC service
  13815. // (no reService flag), nor perform any modification to a table: in short,
  13816. // this is an ORM read-only limited user
  13817. // - you MUST override the default 'synopse' password to a custom value,
  13818. // or at least customize the global AuthAdminDefaultPassword,
  13819. // AuthSupervisorDefaultPassword, AuthUserDefaultPassword variables
  13820. // - of course, you can change and tune the settings of the AuthGroup and
  13821. // AuthUser tables, but only 'Admin' group users will be able to remotely
  13822. // modify the content of those two tables
  13823. class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
  13824. Options: TSQLInitializeTableOptions); override;
  13825. /// corresponding TSQLAccessRights for this authentication group
  13826. // - content is converted into/from text format via AccessRight DB property
  13827. // (so it will be not fixed e.g. by the binary TSQLFieldTables layout, i.e.
  13828. // the MAX_SQLTABLES constant value)
  13829. property SQLAccessRights: TSQLAccessRights read GetSQLAccessRights write SetSQLAccessRights;
  13830. published
  13831. /// the access right identifier, ready to be displayed
  13832. // - the same identifier can be used only once (this column is marked as
  13833. // unique via a "stored AS_UNIQUE" (i.e. "stored false") attribute)
  13834. // - so you can retrieve a TSQLAuthGroup ID from its identifier, as such:
  13835. // ! UserGroupID := fClient.MainFieldID(TSQLAuthGroup,'User');
  13836. property Ident: RawUTF8 index 50 read fIdent write fIdent stored AS_UNIQUE;
  13837. /// the number of minutes a session is kept alive
  13838. property SessionTimeout: integer read fSessionTimeOut write fSessionTimeOut;
  13839. /// a textual representation of a TSQLAccessRights buffer
  13840. property AccessRights: RawUTF8 index 1600 read fAccessRights write fAccessRights;
  13841. end;
  13842. /// class-reference type (metaclass) of the table containing the available
  13843. // user access rights for authentication, defined as a group
  13844. TSQLAuthGroupClass = class of TSQLAuthGroup;
  13845. /// table containing the Users registered for authentication
  13846. // - this class should be added to the TSQLModel, together with TSQLAuthGroup,
  13847. // to allow authentication support
  13848. // - you can inherit from it to add your custom properties to each user info:
  13849. // TSQLModel will search for any class inheriting from TSQLAuthUser to manage
  13850. // per-user authorization data
  13851. // - by default, it won't be accessible remotely by anyone; to enhance security,
  13852. // you could use the TSynValidatePassWord filter to this table
  13853. TSQLAuthUser = class(TSQLRecord)
  13854. protected
  13855. fLogonName: RawUTF8;
  13856. fPasswordHashHexa: RawUTF8;
  13857. fDisplayName: RawUTF8;
  13858. fGroup: TSQLAuthGroup;
  13859. fData: TSQLRawBlob;
  13860. procedure SetPasswordPlain(const Value: RawUTF8);
  13861. /// check if the user can authenticate in its current state
  13862. // - called by TSQLRestServerAuthentication.GetUser() method
  13863. // - this default implementation will return TRUE, i.e. allow the user
  13864. // to log on
  13865. // - override this method to disable user authentication, e.g. if the
  13866. // user is disabled via a custom ORM boolean and date/time field
  13867. function CanUserLog(Ctxt: TSQLRestServerURIContext): boolean; virtual;
  13868. public
  13869. /// static function allowing to compute a hashed password
  13870. // - as expected by this class
  13871. // - defined as virtual so that you may use your own hashing class
  13872. // - you may specify your own values in aHashSalt/aHashRound, to enable
  13873. // PBKDF2_HMAC_SHA256() use instead of plain SHA256(): it would increase
  13874. // security on storage side (reducing brute force attack via rainbow tables)
  13875. class function ComputeHashedPassword(const aPasswordPlain: RawUTF8;
  13876. const aHashSalt: RawUTF8=''; aHashRound: integer=20000): RawUTF8; virtual;
  13877. /// able to set the PasswordHashHexa field from a plain password content
  13878. // - in fact, PasswordHashHexa := SHA256('salt'+PasswordPlain) in UTF-8
  13879. // - use SetPassword() method if you want to customize the hash salt value
  13880. property PasswordPlain: RawUTF8 write SetPasswordPlain;
  13881. /// set the PasswordHashHexa field from a plain password content and salt
  13882. // - use this method to specify aHashSalt/aHashRound values, enabling
  13883. // PBKDF2_HMAC_SHA256() use instead of plain SHA256(): it would increase
  13884. // security on storage side (reducing brute force attack via rainbow tables)
  13885. // - you may use an application specific fixed salt, and/or append the
  13886. // user LogonName to make the challenge unique for each TSQLAuthUser
  13887. // - the default aHashRound=20000 is slow but secure - since the hashing
  13888. // process is expected to be done on client side, you may specify your
  13889. // own higher/slower value, depending on the security level you expect
  13890. procedure SetPassword(const aPasswordPlain, aHashSalt: RawUTF8;
  13891. aHashRound: integer=20000);
  13892. published
  13893. /// the User identification Name, as entered at log-in
  13894. // - the same identifier can be used only once (this column is marked as
  13895. // unique via a "stored AS_UNIQUE" - i.e. "stored false" - attribute), and
  13896. // therefore indexed in the database (e.g. hashed in TSQLRestStorageInMemory)
  13897. property LogonName: RawUTF8 index 20 read fLogonName write fLogonName stored AS_UNIQUE;
  13898. /// the User Name, as may be displayed or printed
  13899. property DisplayName: RawUTF8 index 50 read fDisplayName write fDisplayName;
  13900. /// the hexa encoded associated SHA-256 hash of the password
  13901. // - see TSQLAuthUser.ComputeHashedPassword() or SetPassword() methods
  13902. // - store the SHA-256 32 bytes as 64 hexa chars
  13903. property PasswordHashHexa: RawUTF8 index 64 read fPasswordHashHexa write fPasswordHashHexa;
  13904. /// the associated access rights of this user
  13905. // - access rights are managed by group
  13906. // - in TAuthSession.User instance, GroupRights property will contain a
  13907. // REAL TSQLAuthGroup instance for fast retrieval in TSQLRestServer.URI
  13908. // - note that 'Group' field name is not allowed by SQLite
  13909. property GroupRights: TSQLAuthGroup read fGroup write fGroup;
  13910. /// some custom data, associated to the User
  13911. // - Server application may store here custom data
  13912. // - its content is not used by the framework but 'may' be used by your
  13913. // application
  13914. property Data: TSQLRawBlob read fData write fData;
  13915. end;
  13916. /// class-reference type (metaclass) of a table containing the Users
  13917. // registered for authentication
  13918. // - see also TSQLRestServer.OnAuthenticationUserRetrieve custom event
  13919. TSQLAuthUserClass = class of TSQLAuthUser;
  13920. /// class used to maintain in-memory sessions
  13921. // - this is not a TSQLRecord table so won't be remotely accessible, for
  13922. // performance and security reasons
  13923. // - the User field is a true instance, copy of the corresponding database
  13924. // content (for better speed)
  13925. // - you can inherit from this class, to add custom session process
  13926. TAuthSession = class(TSynPersistent)
  13927. protected
  13928. fUser: TSQLAuthUser;
  13929. fLastAccess64: Int64;
  13930. fID: RawUTF8;
  13931. fIDCardinal: cardinal;
  13932. fTimeOutMS: cardinal;
  13933. fAccessRights: TSQLAccessRights;
  13934. fPrivateKey: RawUTF8;
  13935. fPrivateSalt: RawUTF8;
  13936. fSentHeaders: RawUTF8;
  13937. fRemoteIP: RawUTF8;
  13938. fPrivateSaltHash: Cardinal;
  13939. fLastTimeStamp: Cardinal;
  13940. fExpectedHttpAuthentication: RawUTF8;
  13941. fMethods: TSynMonitorInputOutputObjArray;
  13942. fInterfaces: TSynMonitorInputOutputObjArray;
  13943. function GetUserName: RawUTF8;
  13944. function GetUserID: TID;
  13945. function GetGroupID: TID;
  13946. procedure SaveTo(W: TFileBufferWriter); virtual;
  13947. procedure ComputeProtectedValues; virtual;
  13948. constructor CreateFrom(var P: PAnsiChar; Server: TSQLRestServer); virtual;
  13949. public
  13950. /// initialize a session instance with the supplied TSQLAuthUser instance
  13951. // - this aUser instance will be handled by the class until Destroy
  13952. // - raise an exception on any error
  13953. // - on success, will also retrieve the aUser.Data BLOB field content
  13954. constructor Create(aCtxt: TSQLRestServerURIContext; aUser: TSQLAuthUser); reintroduce; virtual;
  13955. /// will release the User and User.GroupRights instances
  13956. destructor Destroy; override;
  13957. public
  13958. /// the session ID number, as numerical value
  13959. // - never equals to 1 (CONST_AUTHENTICATION_NOT_USED, i.e. authentication
  13960. // mode is not enabled), nor 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED,
  13961. // i.e. session still in handshaking phase)
  13962. property IDCardinal: cardinal read fIDCardinal;
  13963. /// the associated User
  13964. // - this is a true TSQLAuthUser instance, and User.GroupRights will contain
  13965. // also a true TSQLAuthGroup instance
  13966. property User: TSQLAuthUser read fUser;
  13967. /// set by the Access() method to the current GetTickCount64() time stamp
  13968. property LastAccess64: Int64 read fLastAccess64;
  13969. /// copy of the associated user access rights
  13970. // - extracted from User.TSQLAuthGroup.SQLAccessRights
  13971. property AccessRights: TSQLAccessRights read fAccessRights;
  13972. /// the hexadecimal private key as returned to the connected client
  13973. // as 'SessionID+PrivateKey'
  13974. property PrivateKey: RawUTF8 read fPrivateKey;
  13975. /// the transmitted HTTP headers, if any
  13976. // - can contain e.g. 'RemoteIp: 127.0.0.1' or 'User-Agent: Mozilla/4.0'
  13977. property SentHeaders: RawUTF8 read fSentHeaders;
  13978. /// per-session statistics about method-based services
  13979. // - Methods[] follows TSQLRestServer.fPublishedMethod[] array
  13980. // - is initialized and maintained only if mlSessions is defined in
  13981. // TSQLRestServer.StatLevels property
  13982. property Methods: TSynMonitorInputOutputObjArray read fMethods;
  13983. /// per-session statistics about interface-based services
  13984. // - Interfaces[] follows TSQLRestServer.Services.fListInterfaceMethod[] array
  13985. // - is initialized and maintained only if mlSessions is defined in
  13986. // TSQLRestServer.StatLevels property
  13987. property Interfaces: TSynMonitorInputOutputObjArray read fInterfaces;
  13988. published
  13989. /// the session ID number, as text
  13990. property ID: RawUTF8 read fID;
  13991. /// the associated User Name, as in User.LogonName
  13992. property UserName: RawUTF8 read GetUserName;
  13993. /// the associated User ID, as in User.ID
  13994. property UserID: TID read GetUserID;
  13995. /// the associated Group ID, as in User.GroupRights.ID
  13996. property GroupID: TID read GetGroupID;
  13997. /// the number of milliseconds a session is kept alive
  13998. // - extracted from User.TSQLAuthGroup.SessionTimeout
  13999. // - allow direct comparison with GetTickCount64() API call
  14000. property TimeoutMS: cardinal read fTimeOutMS;
  14001. /// the remote IP, if any
  14002. // - is extracted from SentHeaders properties
  14003. property RemoteIP: RawUTF8 read fRemoteIP;
  14004. end;
  14005. /// class-reference type (metaclass) used to define overridden session instances
  14006. // - since all sessions data remain in memory, ensure they are not taking too
  14007. // much resource (memory or process time)
  14008. // - if you plan to use session persistence, ensure you override the
  14009. // TAuthSession.SaveTo/CreateFrom methods in the inherited class
  14010. TAuthSessionClass = class of TAuthSession;
  14011. /// class-reference type (metaclass) used to define an authentication scheme
  14012. TSQLRestServerAuthenticationClass = class of TSQLRestServerAuthentication;
  14013. /// maintain a list of TSQLRestServerAuthentication instances
  14014. TSQLRestServerAuthenticationDynArray = array of TSQLRestServerAuthentication;
  14015. /// define how TSQLRestServerAuthentication.ClientSetUser() should interpret
  14016. // the supplied password
  14017. // - passClear means that the password is not encrypted, e.g. as entered
  14018. // by the user in the login screen
  14019. // - passHashed means that the passwod is already hashed as in
  14020. // TSQLAuthUser.PasswordHashHexa i.e. SHA256('salt'+Value)
  14021. // - passKerberosSPN indicates that the password is the Kerberos SPN domain
  14022. TSQLRestServerAuthenticationClientSetUserPassword = (
  14023. passClear, passHashed, passKerberosSPN);
  14024. /// optional behavior of TSQLRestServerAuthentication class
  14025. // - by default, saoUserByLogonOrID is set, allowing
  14026. // TSQLRestServerAuthentication.GetUser() to retrieve the TSQLAuthUser by
  14027. // logon name or by ID, if the supplied logon name is an integer
  14028. // - if saoHandleUnknownLogonAsStar is defined, any user successfully
  14029. // authenticated could be logged with the same ID (and authorization)
  14030. // than TSQLAuthUser.Logon='*' - of course, this is meaningfull only with
  14031. // an external credential check (e.g. via SSPI or Active Directory)
  14032. TSQLRestServerAuthenticationOption = (
  14033. saoUserByLogonOrID, saoHandleUnknownLogonAsStar);
  14034. /// defines the optional behavior of TSQLRestServerAuthentication class
  14035. TSQLRestServerAuthenticationOptions = set of TSQLRestServerAuthenticationOption;
  14036. /// abstract class used to implement server-side authentication in TSQLRestServer
  14037. // - inherit from this class to implement expected authentication scheme
  14038. TSQLRestServerAuthentication = class
  14039. protected
  14040. fServer: TSQLRestServer;
  14041. fOptions: TSQLRestServerAuthenticationOptions;
  14042. // GET ModelRoot/auth?UserName=...&Session=... -> release session
  14043. function AuthSessionRelease(Ctxt: TSQLRestServerURIContext): boolean;
  14044. /// retrieve an User instance from its logon name
  14045. // - should return nil if not found
  14046. // - this default implementation will retrieve it from ORM, and
  14047. // call TSQLAuthUser.CanUserLog() to ensure authentication is allowed
  14048. // - if aUserName is an integer, it will try to retrieve it from ORM using
  14049. // the supplied value as its TSQLAuthUser.ID: it may be convenient when the
  14050. // client is not an end-user application but a mORMot server (in a cloud
  14051. // architecture), since it would benefit from local ORM cache
  14052. // - you can override this method and return an on-the-fly created value
  14053. // as a TSQLRestServer.SQLAuthUserClass instance (i.e. not persisted
  14054. // in database nor retrieved by ORM), but the resulting TSQLAuthUser
  14055. // must have its ID and LogonName properties set with unique values (which
  14056. // will be used to identify it for a later call and session owner
  14057. // identification), and its GroupRights property must not yet contain a real
  14058. // TSQLAuthGroup instance, just a TSQLAuthGroup(aGroupID) value (as directly
  14059. // retrieved from the ORM) - TAuthSession.Create will retrieve the instance
  14060. // - another possibility, orthogonal to all TSQLRestServerAuthentication
  14061. // classes, may be to define a TSQLRestServer.OnAuthenticationUserRetrieve
  14062. // custom event
  14063. function GetUser(Ctxt: TSQLRestServerURIContext;
  14064. const aUserName: RawUTF8): TSQLAuthUser; virtual;
  14065. /// create a session on the server for a given user
  14066. // - this default implementation will call fServer.SessionCreate() and
  14067. // return a '{"result":"HEXASALT","logonname":"UserName"}' JSON content
  14068. // and will always call User.Free
  14069. // - on failure, will call TSQLRestServerURIContext.AuthenticationFailed()
  14070. // with afSessionAlreadyStartedForThisUser or afSessionCreationAborted reason
  14071. procedure SessionCreate(Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser); virtual;
  14072. /// abstract method which will be called by ClientSetUser() to process the
  14073. // authentication step on the client side
  14074. // - at call, a TSQLAuthUser instance will be supplied, with LogonName set
  14075. // with aUserName and PasswordHashHexa with a SHA-256 hash of aPassword
  14076. // - override with the expected method, returning the session key on success
  14077. class function ClientComputeSessionKey(Sender: TSQLRestClientURI;
  14078. User: TSQLAuthUser): RawUTF8; virtual; abstract;
  14079. /// is called by ClientComputeSessionKey() overriden method to execute the
  14080. // root/Auth service with the supplied parameters, then retrieve and
  14081. // decode the "result": session key and any other values (e.g. "version")
  14082. class function ClientGetSessionKey(Sender: TSQLRestClientURI;
  14083. User: TSQLAuthUser; const aNameValueParameters: array of const): RawUTF8; virtual;
  14084. public
  14085. /// initialize the authentication method to a specified server
  14086. // - you can define several authentication schemes for the same server
  14087. constructor Create(aServer: TSQLRestServer); virtual;
  14088. /// called by the Server to implement the Auth RESTful method
  14089. // - overridden method shall return TRUE if the request has been handled
  14090. // - returns FALSE to let the next registered TSQLRestServerAuthentication
  14091. // class to try implementing the content
  14092. // - Ctxt.Parameters has been tested to contain an UserName=... value
  14093. // - method execution is protected by TSQLRestServer.fSessions.Lock
  14094. function Auth(Ctxt: TSQLRestServerURIContext): boolean; virtual; abstract;
  14095. /// called by the Server to check if the execution context match a session
  14096. // - returns a session instance corresponding to the remote request
  14097. // - returns nil if this remote request does not match this authentication
  14098. // - method execution is protected by TSQLRestServer.fSessions.Lock
  14099. function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; virtual; abstract;
  14100. /// allow to tune the authentication process
  14101. // - default value is [saoUserByLogonOrID]
  14102. property Options: TSQLRestServerAuthenticationOptions read fOptions write fOptions;
  14103. /// class method to be used on client side to create a remote session
  14104. // - call this method instead of TSQLRestClientURI.SetUser() if you need
  14105. // a custom authentication class
  14106. // - if saoUserByLogonOrID is defined in the server Options, aUserName may
  14107. // be a TSQLAuthUser.ID and not a TSQLAuthUser.LogonName
  14108. // - if passClear is used, you may specify aHashSalt and aHashRound,
  14109. // to enable PBKDF2_HMAC_SHA256() use instead of plain SHA256(), and increase
  14110. // security on storage side (reducing brute force attack via rainbow tables)
  14111. // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth()
  14112. // published method to create a session for this user
  14113. // - returns true on success
  14114. class function ClientSetUser(Sender: TSQLRestClientURI;
  14115. const aUserName, aPassword: RawUTF8;
  14116. aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword=passClear;
  14117. const aHashSalt: RawUTF8=''; aHashRound: integer=20000): boolean; virtual;
  14118. /// class method to be called on client side to sign an URI
  14119. // - used by TSQLRestClientURI.URI()
  14120. // - shall match the method as expected by RetrieveSession() virtual method
  14121. class procedure ClientSessionSign(Sender: TSQLRestClientURI;
  14122. var Call: TSQLRestURIParams); virtual; abstract;
  14123. end;
  14124. /// weak authentication scheme using URL-level parameter
  14125. TSQLRestServerAuthenticationURI = class(TSQLRestServerAuthentication)
  14126. public
  14127. /// will check URI-level signature
  14128. // - retrieve the session ID from 'session_signature=...' parameter
  14129. function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
  14130. /// class method to be called on client side to add the SessionID to the URI
  14131. // - append '&session_signature=SessionID' to the url
  14132. class procedure ClientSessionSign(Sender: TSQLRestClientURI;
  14133. var Call: TSQLRestURIParams); override;
  14134. end;
  14135. /// secure authentication scheme using URL-level digital signature
  14136. // - expected format of session_signature is
  14137. // !Hexa8(SessionID)+
  14138. // !Hexa8(TimeStamp)+
  14139. // !Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+
  14140. // ! Hexa8(TimeStamp)+url))
  14141. TSQLRestServerAuthenticationSignedURI = class(TSQLRestServerAuthenticationURI)
  14142. protected
  14143. fNoTimeStampCoherencyCheck: Boolean;
  14144. fTimeStampCoherencySeconds: cardinal;
  14145. procedure SetNoTimeStampCoherencyCheck(value: boolean);
  14146. public
  14147. /// initialize the authentication method to a specified server
  14148. constructor Create(aServer: TSQLRestServer); override;
  14149. /// will check URI-level signature
  14150. // - check session_signature=... parameter to be a valid digital signature
  14151. function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
  14152. /// class method to be called on client side to sign an URI
  14153. // - generate the digital signature as expected by overridden RetrieveSession()
  14154. // - timestamp resolution is about 256 ms in the current implementation
  14155. class procedure ClientSessionSign(Sender: TSQLRestClientURI;
  14156. var Call: TSQLRestURIParams); override;
  14157. /// allow any order when creating sessions
  14158. // - by default, signed sessions are expected to be sequential, and new
  14159. // signed session signature can't be older in time than the last one,
  14160. // with a tolerance of TimeStampCoherencySeconds
  14161. // - but if your client is asynchronous (e.g. for AJAX requests), session
  14162. // may be rejected due to the delay involved on the client side: you can set
  14163. // this property to TRUE to enabled a weaker but more tolerant behavior
  14164. // ! (aServer.AuthenticationRegister(TSQLRestServerAuthenticationDefault) as
  14165. // ! TSQLRestServerAuthenticationSignedURI).NoTimeStampCoherencyCheck := true;
  14166. property NoTimeStampCoherencyCheck: Boolean read fNoTimeStampCoherencyCheck
  14167. write SetNoTimeStampCoherencyCheck;
  14168. /// time tolerance in seconds for the signature timestamps coherency check
  14169. // - by default, signed sessions are expected to be sequential, and new
  14170. // signed session signature can't be older in time than the last one,
  14171. // with a tolerance time defined by this property
  14172. // - default value is 5 seconds, which cover most kind of clients (AJAX or
  14173. // WebSockets), even over a slow Internet connection
  14174. property TimeStampCoherencySeconds: cardinal read fTimeStampCoherencySeconds
  14175. write fTimeStampCoherencySeconds;
  14176. end;
  14177. /// mORMot secure RESTful authentication scheme
  14178. // - this method will use a password stored via safe SHA-256 hashing in the
  14179. // TSQLAuthUser ORM table
  14180. TSQLRestServerAuthenticationDefault = class(TSQLRestServerAuthenticationSignedURI)
  14181. protected
  14182. /// check a supplied password content
  14183. // - will match ClientComputeSessionKey() algorithm as overridden here, i.e.
  14184. // a SHA-256 based signature with a 10 minutes activation window
  14185. function CheckPassword(Ctxt: TSQLRestServerURIContext;
  14186. User: TSQLAuthUser; const aClientNonce, aPassWord: RawUTF8): boolean; virtual;
  14187. /// class method used on client side to create a remote session
  14188. // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth()
  14189. // published method to create a session for this user: so
  14190. // TSQLRestServerAuthenticationDefault should be registered on server side
  14191. // - User.LogonName and User.PasswordHashHexa will be checked
  14192. class function ClientComputeSessionKey(Sender: TSQLRestClientURI;
  14193. User: TSQLAuthUser): RawUTF8; override;
  14194. public
  14195. /// will try to handle the Auth RESTful method with mORMot authentication
  14196. // - to be called in a two pass "challenging" algorithm:
  14197. // $ GET ModelRoot/auth?UserName=...
  14198. // $ -> returns an hexadecimal nonce contents (valid for 5 minutes)
  14199. // $ GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=...
  14200. // $ -> if password is OK, will open the corresponding session
  14201. // $ and return 'SessionID+HexaSessionPrivateKey'
  14202. // The Password parameter as sent for the 2nd request will be computed as
  14203. // ! Sha256(ModelRoot+Nonce+ClientNonce+UserName+Sha256('salt'+PassWord))
  14204. // - the returned HexaSessionPrivateKey content will identify the current
  14205. // user logged and its corresponding session (the same user may have several
  14206. // sessions opened at once, each with its own private key)
  14207. // - then the private session key must be added to every query sent to
  14208. // the server as a session_signature=???? parameter, which will be computed
  14209. // as such:
  14210. // $ ModelRoot/url?A=1&B=2&session_signature=012345670123456701234567
  14211. // were the session_signature= parameter will be computed as such:
  14212. // ! Hexa8(SessionID)+Hexa8(TimeStamp)+
  14213. // ! Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+
  14214. // ! Hexa8(TimeStamp)+url))
  14215. // ! with url='ModelRoot/url?A=1&B=2'
  14216. // this query authentication uses crc32 for hashing instead of SHA-256 in
  14217. // in order to lower the Server-side CPU consumption; the salted password
  14218. // (i.e. TSQLAuthUser.PasswordHashHexa) and client-side TimeStamp are
  14219. // inserted inside the session_signature calculation to prevent naive
  14220. // man-in-the-middle attack (MITM)
  14221. // - the session ID will be used to retrieve the rights associated with the
  14222. // user which opened the session via a successful call to the Auth service
  14223. // - when you don't need the session any more (e.g. if the TSQLRestClientURI
  14224. // instance is destroyed), you can call the service as such:
  14225. // $ GET ModelRoot/auth?UserName=...&Session=...
  14226. // - for a way of computing SHA-256 in JavaScript, see for instance
  14227. // @http://www.webtoolkit.info/javascript-sha256.html
  14228. function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  14229. end;
  14230. /// mORMot weak RESTful authentication scheme
  14231. // - this method will authenticate with a given username, but no signature
  14232. // - on client side, this scheme is not called by TSQLRestClientURI.SetUser()
  14233. // method - so you have to write:
  14234. // ! TSQLRestServerAuthenticationNone.ClientSetUser(Client,'User','');
  14235. TSQLRestServerAuthenticationNone = class(TSQLRestServerAuthenticationURI)
  14236. protected
  14237. /// class method used on client side to create a remote session
  14238. // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth()
  14239. // published method to create a session for this user: so
  14240. // TSQLRestServerAuthenticationNone should be registered on server side
  14241. // - will check User.LogonName, but User.PasswordHashHexa will be ignored
  14242. class function ClientComputeSessionKey(Sender: TSQLRestClientURI;
  14243. User: TSQLAuthUser): RawUTF8; override;
  14244. public
  14245. /// will try to handle the Auth RESTful method with mORMot authentication
  14246. // - to be called in a weak one pass request:
  14247. // $ GET ModelRoot/auth?UserName=...
  14248. // $ -> if the specified user name exists, will open the corresponding
  14249. // $ session and return 'SessionID+HexaSessionPrivateKey'
  14250. function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  14251. end;
  14252. /// abstract class for implementing HTTP authentication
  14253. // - do not use this abstract class, but e.g. TSQLRestServerAuthenticationHttpBasic
  14254. // - this class will transmit the session_signature as HTTP cookie, not at
  14255. // URI level, so is expected to be used only from browsers or old clients
  14256. TSQLRestServerAuthenticationHttpAbstract = class(TSQLRestServerAuthentication)
  14257. protected
  14258. /// should be overriden according to the HTTP authentication scheme
  14259. class function ComputeAuthenticateHeader(
  14260. const aUserName,aPasswordClear: RawUTF8): RawUTF8; virtual; abstract;
  14261. public
  14262. /// will check the caller signature
  14263. // - retrieve the session ID from "Cookie: mORMot_session_signature=..." HTTP header
  14264. function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
  14265. /// class method to be called on client side to sign an URI in Auth Basic
  14266. // resolution is about 256 ms in the current implementation
  14267. // - set "Cookie: mORMot_session_signature=..." HTTP header
  14268. class procedure ClientSessionSign(Sender: TSQLRestClientURI;
  14269. var Call: TSQLRestURIParams); override;
  14270. /// class method to be used on client side to create a remote session
  14271. // - call TSQLRestServerAuthenticationHttpBasic.ClientSetUser() instead of
  14272. // TSQLRestClientURI.SetUser(), and never the method of this abstract class
  14273. // - needs the plain aPassword, so aPasswordKind should be passClear
  14274. // - returns true on success
  14275. class function ClientSetUser(Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8;
  14276. aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword=passClear;
  14277. const aHashSalt: RawUTF8=''; aHashRound: integer=20000): boolean; override;
  14278. /// class method to be used on client side to force the HTTP header for
  14279. // the corresponding HTTP authentication, without creating any remote session
  14280. // - call virtual protected method ComputeAuthenticateHeader()
  14281. // - here the password should be given as clear content
  14282. // - potential use case is to use a mORMot client through a HTTPS proxy,
  14283. // e.g. with TSQLRestServerAuthenticationHttpBasic authentication
  14284. // - then you can use TSQLRestServerAuthentication*.ClientSetUser() to
  14285. // define any another "mORMot only" authentication
  14286. // - this method is also called by the ClientSetUser() method of this class
  14287. // for a full client + server authentication via HTTP
  14288. // TSQLRestServerAuthenticationHttp*.ClientSetUser()
  14289. class procedure ClientSetUserHttpOnly(Sender: TSQLRestClientURI;
  14290. const aUserName, aPasswordClear: RawUTF8); virtual;
  14291. end;
  14292. /// authentication using HTTP Basic scheme
  14293. // - this protocol send both name and password as clear (just base-64 encoded)
  14294. // so should only be used over SSL / HTTPS, or for compatibility reasons
  14295. // - will rely on TSQLRestServerAuthenticationNone for authorization
  14296. // - on client side, this scheme is not called by TSQLRestClientURI.SetUser()
  14297. // method - so you have to write:
  14298. // ! TSQLRestServerAuthenticationHttpBasic.ClientSetUser(Client,'User','password');
  14299. // - for a remote proxy-only authentication (without creating any mORMot
  14300. // session), you can write:
  14301. // ! TSQLRestServerAuthenticationHttpBasic.ClientSetUserHttpOnly(Client,'proxyUser','proxyPass');
  14302. TSQLRestServerAuthenticationHttpBasic = class(TSQLRestServerAuthenticationHttpAbstract)
  14303. protected
  14304. /// this overriden method returns "Authorization: Basic ...." HTTP header
  14305. class function ComputeAuthenticateHeader(
  14306. const aUserName,aPasswordClear: RawUTF8): RawUTF8; override;
  14307. /// decode "Authorization: Basic ...." header
  14308. // - you could implement you own password transmission pattern, by
  14309. // overriding both ComputeAuthenticateHeader and GetUserPassFromInHead methods
  14310. class function GetUserPassFromInHead(Ctxt: TSQLRestServerURIContext;
  14311. out userPass,user,pass: RawUTF8): boolean; virtual;
  14312. /// check a supplied password content
  14313. // - this default implementation will use the SHA-256 hash value stored
  14314. // within User.PasswordHashHexa
  14315. // - you can override this method to provide your own password check
  14316. // mechanism, for the given TSQLAuthUser instance
  14317. function CheckPassword(Ctxt: TSQLRestServerURIContext;
  14318. User: TSQLAuthUser; const aPassWord: RawUTF8): boolean; virtual;
  14319. public
  14320. /// will check URI-level signature
  14321. // - retrieve the session ID from 'session_signature=...' parameter
  14322. // - will also check incoming "Authorization: Basic ...." HTTP header
  14323. function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
  14324. /// handle the Auth RESTful method with HTTP Basic
  14325. // - will first return HTML_UNAUTHORIZED (401), then expect user and password
  14326. // to be supplied as incoming "Authorization: Basic ...." headers
  14327. function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  14328. end;
  14329. {$ifdef SSPIAUTH}
  14330. /// authentication of the current logged user using Windows Security Support
  14331. // Provider Interface (SSPI)
  14332. // - is able to authenticate the currently logged user on the client side,
  14333. // using either NTLM or Kerberos - it would allow to safely authenticate
  14334. // on a mORMot server without prompting the user to enter its password
  14335. // - if ClientSetUser() receives aUserName as '', aPassword should be either
  14336. // '' if you expect NTLM authentication to take place, or contain the SPN
  14337. // registration (e.g. 'mymormotservice/myserver.mydomain.tld') for Kerberos
  14338. // authentication
  14339. // - if ClientSetUser() receives aUserName as 'DomainName\UserName', then
  14340. // authentication will take place on the specified domain, with aPassword
  14341. // as plain password value
  14342. TSQLRestServerAuthenticationSSPI = class(TSQLRestServerAuthenticationSignedURI)
  14343. protected
  14344. /// Windows built-in authentication
  14345. // - holds information between calls to ServerSSPIAuth
  14346. fSSPIAuthContexts: TSecContextDynArray;
  14347. /// class method used on client side to create a remote session
  14348. // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth()
  14349. // published method to create a session for this user: so
  14350. // TSQLRestServerAuthenticationSSPI should be registered on server side
  14351. // - Windows SSPI authentication will be performed - in this case,
  14352. // table TSQLAuthUser shall contain an entry for the logged Windows user,
  14353. // with the LoginName in form 'DomainName\UserName'
  14354. // - if User.LogonName is '', then User.PasswordHashHexa is '' for
  14355. // NTLM authentication, or the SPN registration for Kerberos authentication
  14356. // - if User.LogonName is set as 'DomainName\UserName', then authentication
  14357. // would take place on the specified domain, with User.PasswordHashHexa as
  14358. // plain password
  14359. class function ClientComputeSessionKey(Sender: TSQLRestClientURI;
  14360. User: TSQLAuthUser): RawUTF8; override;
  14361. public
  14362. /// initialize the authentication method to a specified server
  14363. constructor Create(aServer: TSQLRestServer); override;
  14364. /// finalize internal memory structures
  14365. destructor Destroy; override;
  14366. /// will try to handle the Auth RESTful method with Windows SSPI API
  14367. // - to be called in a two pass algorithm, used to cypher the password
  14368. // - the client-side logged user will be identified as valid, according
  14369. // to a Windows SSPI API secure challenge
  14370. function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  14371. end;
  14372. {$endif SSPIAUTH}
  14373. /// supported REST authentication schemes
  14374. // - used by the overloaded TSQLHttpServer.Create(TSQLHttpServerDefinition)
  14375. // constructor in mORMotHttpServer.pas, and also in dddInfraSettings.pas
  14376. // - asSSPI won't be defined under Linux, since it is a Windows-centric feature
  14377. TSQLHttpServerRestAuthentication = (
  14378. adDefault, adHttpBasic, adWeak, adSSPI);
  14379. /// parameters supplied to publish a TSQLRestServer via HTTP
  14380. // - used by the overloaded TSQLHttpServer.Create(TSQLHttpServerDefinition)
  14381. // constructor in mORMotHttpServer.pas, and also in dddInfraSettings.pas
  14382. TSQLHttpServerDefinition = class(TSynPersistentWithPassword)
  14383. protected
  14384. FBindPort: RawByteString;
  14385. FAuthentication: TSQLHttpServerRestAuthentication;
  14386. FEnableCORS: boolean;
  14387. FThreadCount: byte;
  14388. FHttps: boolean;
  14389. FHttpSysQueueName: SynUnicode;
  14390. published
  14391. /// defines the port to be used for REST publishing
  14392. // - may include an optional IP address to bind, e.g. '127.0.0.1:8888'
  14393. property BindPort: RawByteString read FBindPort write FBindPort;
  14394. /// which authentication is expected to be published
  14395. property Authentication: TSQLHttpServerRestAuthentication
  14396. read FAuthentication write FAuthentication;
  14397. /// allow Cross-origin resource sharing (CORS) access
  14398. // - set this property to TRUE if you want to be able to access the
  14399. // REST methods from an HTML5 application hosted in another location
  14400. // - i.e. will set the following HTTP header:
  14401. // ! Access-Control-Allow-Origin: *
  14402. property EnableCORS: boolean read FEnableCORS write FEnableCORS;
  14403. /// how many threads the thread pool associated with this HTTP server
  14404. // should create
  14405. // - if set to 0, will use default value 32
  14406. // - this parameter may be ignored depending on the actual HTTP
  14407. // server used, which may not have any thread pool
  14408. property ThreadCount: byte read fThreadCount write fThreadCount;
  14409. /// defines if https:// protocol should be used
  14410. // - implemented only by http.sys server under Windows, not by socket servers
  14411. property Https: boolean read FHttps write FHttps;
  14412. /// the displayed name in the http.sys queue
  14413. // - used only by http.sys server under Windows, not by socket-based servers
  14414. property HttpSysQueueName: SynUnicode read FHttpSysQueueName write FHttpSysQueueName;
  14415. /// if defined, this HTTP server will use WebSockets, and our secure
  14416. // encrypted binary protocol
  14417. // - when stored in the settings JSON file, the password will be safely
  14418. // encrypted as defined by TSynPersistentWithPassword
  14419. // - use the inherited PlainPassword property to set or read its value
  14420. property WebSocketPassword: RawUTF8 read fPassWord write fPassWord;
  14421. end;
  14422. /// TSynAuthentication* class using TSQLAuthUser/TSQLAuthGroup for credentials
  14423. // - could be used e.g. for SynDBRemote access in conjunction with mORMot
  14424. TSynAuthenticationRest = class(TSynAuthenticationAbstract)
  14425. protected
  14426. fServer: TSQLRestServer;
  14427. fAllowedGroups: TIntegerDynArray;
  14428. function GetPassword(const UserName: RawUTF8; out Password: RawUTF8): boolean; override;
  14429. function GetUsersCount: integer; override;
  14430. public
  14431. /// initialize the authentication scheme
  14432. // - you can optionally set the groups allowing to use SynDBRemote - if none
  14433. // is specify, username/password is enough
  14434. constructor Create(aServer: TSQLRestServer; const aAllowedGroups: array of integer); reintroduce;
  14435. /// add some new groups to validate an user authentication
  14436. procedure RegisterAllowedGroups(const aAllowedGroups: array of integer);
  14437. /// to be used to compute a Hash on the client side, for a given Token
  14438. // - the password will be hashed as expected by the GetPassword() method
  14439. class function ComputeHash(Token: Int64; const UserName,PassWord: RawUTF8): cardinal; override;
  14440. end;
  14441. /// common ancestor for tracking TSQLRecord modifications
  14442. // - e.g. TSQLRecordHistory and TSQLRecordVersion will inherit from this class
  14443. // to track TSQLRecord changes
  14444. TSQLRecordModification = class(TSQLRecord)
  14445. protected
  14446. fModifiedRecord: TID;
  14447. fTimeStamp: TModTime;
  14448. public
  14449. /// returns the modified record table, as stored in ModifiedRecord
  14450. function ModifiedTable(Model: TSQLModel): TSQLRecordClass;
  14451. {$ifdef HASINLINE}inline;{$endif}
  14452. /// returns the record table index in the TSQLModel, as stored in ModifiedRecord
  14453. function ModifiedTableIndex: integer;
  14454. {$ifdef HASINLINE}inline;{$endif}
  14455. /// returns the modified record ID, as stored in ModifiedRecord
  14456. function ModifiedID: TID;
  14457. {$ifdef HASINLINE}inline;{$endif}
  14458. published
  14459. /// identifies the modified record
  14460. // - ID and table index in TSQLModel is stored as one RecordRef integer
  14461. // - you can use ModifiedTable/ModifiedID to retrieve the TSQLRecord item
  14462. // - in case of the record deletion, all matching TSQLRecordHistory won't
  14463. // be touched by TSQLRestServer.AfterDeleteForceCoherency(): so this
  14464. // property is a plain TID/Int64, not a TRecordReference field
  14465. property ModifiedRecord: TID read fModifiedRecord write fModifiedRecord;
  14466. /// when the modification was recorded
  14467. // - even if in most cases, this timestamp may be synchronized over TSQLRest
  14468. // instances (thanks to TSQLRestClientURI.ServerTimeStampSynchronize), it
  14469. // is not safe to use this field as absolute: you should rather rely on
  14470. // pure monotonic ID/RowID increasing values (see e.g. TSQLRecordVersion)
  14471. property TimeStamp: TModTime read fTimeStamp write fTimeStamp;
  14472. end;
  14473. /// common ancestor for tracking changes on TSQLRecord tables
  14474. // - used by TSQLRestServer.TrackChanges() method for simple fields history
  14475. // - TSQLRestServer.InternalUpdateEvent will use this table to store individual
  14476. // row changes as SentDataJSON, then will compress them in History BLOB
  14477. // - note that any layout change of the tracked TSQLRecord table (e.g. adding
  14478. // a new property) would break the internal data format, so will void the table
  14479. TSQLRecordHistory = class(TSQLRecordModification)
  14480. protected
  14481. fEvent: TSQLHistoryEvent;
  14482. fSentData: RawUTF8;
  14483. fHistory: TSQLRawBlob;
  14484. // BLOB storage layout is: RTTIheader + offsets + recordsdata
  14485. fHistoryModel: TSQLModel;
  14486. fHistoryTable: TSQLRecordClass;
  14487. fHistoryTableIndex: integer;
  14488. fHistoryUncompressed: RawByteString;
  14489. fHistoryUncompressedCount: integer;
  14490. fHistoryUncompressedOffset: TIntegerDynArray;
  14491. fHistoryAdd: TFileBufferWriter;
  14492. fHistoryAddCount: integer;
  14493. fHistoryAddOffset: TIntegerDynArray;
  14494. public
  14495. /// load the change history of a given record
  14496. // - then you can use HistoryGetLast, HistoryCount or HistoryGet() to access
  14497. // all previous stored versions
  14498. constructor CreateHistory(aClient: TSQLRest; aTable: TSQLRecordClass; aID: TID);
  14499. /// finalize any internal memory
  14500. destructor Destroy; override;
  14501. /// called when the associated table is created in the database
  14502. // - create index on History(ModifiedRecord,Event) for process speed-up
  14503. class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
  14504. Options: TSQLInitializeTableOptions); override;
  14505. public
  14506. /// prepare to access the History BLOB content
  14507. // - ModifiedRecord should have been set to a proper value
  14508. // - returns FALSE if the History BLOB is incorrect (e.g. TSQLRecord
  14509. // layout changed): caller shall flush all previous history
  14510. function HistoryOpen(Model: TSQLModel): boolean;
  14511. /// returns how many revisions are stored in the History BLOB
  14512. // - HistoryOpen() or CreateHistory() should have been called before
  14513. // - this method will ignore any previous HistoryAdd() call
  14514. function HistoryCount: integer;
  14515. /// retrieve an historical version
  14516. // - HistoryOpen() or CreateHistory() should have been called before
  14517. // - this method will ignore any previous HistoryAdd() call
  14518. // - if Rec=nil, will only retrieve Event and TimeStamp
  14519. // - if Rec is set, will fill all simple properties of this TSQLRecord
  14520. function HistoryGet(Index: integer; out Event: TSQLHistoryEvent;
  14521. out TimeStamp: TModTime; Rec: TSQLRecord): boolean; overload;
  14522. /// retrieve an historical version
  14523. // - HistoryOpen() or CreateHistory() should have been called before
  14524. // - this method will ignore any previous HistoryAdd() call
  14525. // - will fill all simple properties of the supplied TSQLRecord instance
  14526. function HistoryGet(Index: integer; Rec: TSQLRecord): boolean; overload;
  14527. /// retrieve an historical version
  14528. // - HistoryOpen() or CreateHistory() should have been called before
  14529. // - this method will ignore any previous HistoryAdd() call
  14530. // - will return either nil, or a TSQLRecord with all simple properties set
  14531. function HistoryGet(Index: integer): TSQLRecord; overload;
  14532. /// retrieve the latest stored historical version
  14533. // - HistoryOpen() or CreateHistory() should have been called before
  14534. // - this method will ignore any previous HistoryAdd() call
  14535. // - you should not have to use it, since a TSQLRest.Retrieve() is faster
  14536. function HistoryGetLast(Rec: TSQLRecord): boolean; overload;
  14537. /// retrieve the latest stored historical version
  14538. // - HistoryOpen() or CreateHistory() should have been called before,
  14539. // otherwise it will return nil
  14540. // - this method will ignore any previous HistoryAdd() call
  14541. // - you should not have to use it, since a TSQLRest.Retrieve() is faster
  14542. function HistoryGetLast: TSQLRecord; overload;
  14543. /// add a record content to the History BLOB
  14544. // - HistoryOpen() should have been called before using this method -
  14545. // CreateHistory() won't allow history modification
  14546. // - use then HistorySave() to compress and replace the History field
  14547. procedure HistoryAdd(Rec: TSQLRecord; Hist: TSQLRecordHistory);
  14548. /// update the History BLOB field content
  14549. // - HistoryOpen() should have been called before using this method -
  14550. // CreateHistory() won't allow history modification
  14551. // - if HistoryAdd() has not been used, returns false
  14552. // - ID field should have been set for proper persistence on Server
  14553. // - otherwise compress the data into History BLOB, deleting the oldest
  14554. // versions if resulting size is biggger than expected, and returns true
  14555. // - if Server is set, write save the History BLOB to database
  14556. // - if Server and LastRec are set, its content will be compared with the
  14557. // current record in DB (via a Retrieve() call) and stored: it will allow
  14558. // to circumvent any issue about inconsistent use of tracking, e.g. if the
  14559. // database has been modified directly, by-passing the ORM
  14560. function HistorySave(Server: TSQLRestServer;
  14561. LastRec: TSQLRecord=nil): boolean;
  14562. published
  14563. /// the kind of modification stored
  14564. // - is heArchiveBlob when this record stores the compress BLOB in History
  14565. // - otherwise, SentDataJSON may contain the latest values as JSON
  14566. property Event: TSQLHistoryEvent read fEvent write fEvent;
  14567. /// for heAdd/heUpdate, the data is stored as JSON
  14568. // - note that we defined a default maximum size of 4KB for this column,
  14569. // to avoid using a CLOB here - perhaps it may not be enough for huge
  14570. // records - feedback is welcome...
  14571. property SentDataJSON: RawUTF8 index 4000 read fSentData write fSentData;
  14572. /// after some events are written as individual SentData content, they
  14573. // will be gathered and compressed within one BLOB field
  14574. // - use HistoryOpen/HistoryCount/HistoryGet to access the stored data after
  14575. // a call to CreateHistory() constructor
  14576. // - as any BLOB field, this one won't be retrieved by default: use
  14577. // explicitly TSQLRest.RetrieveBlobFields(aRecordHistory) to get it if you
  14578. // want to access it directly, and not via CreateHistory()
  14579. property History: TSQLRawBlob read fHistory write fHistory;
  14580. end;
  14581. /// class-reference type (metaclass) to specify the storage table to be used
  14582. // for tracking TSQLRecord changes
  14583. // - you can create your custom type from TSQLRecordHistory, even for a
  14584. // particular table, to split the tracked changes storage in several tables:
  14585. // ! type
  14586. // ! TSQLRecordMyHistory = class(TSQLRecordHistory);
  14587. // - as expected by TSQLRestServer.TrackChanges() method
  14588. TSQLRecordHistoryClass = class of TSQLRecordHistory;
  14589. /// ORM table used to store the deleted items of a versioned table
  14590. // - the ID/RowID primary key of this table would be the version number
  14591. // (i.e. value computed by TSQLRestServer.InternalRecordVersionCompute),
  14592. // mapped with the corresponding 'TableIndex shl 58' (so that e.g.
  14593. // TSQLRestServer.RecordVersionSynchronizeToBatch() could easily ask for the
  14594. // deleted rows of a given table with a single WHERE clause on the ID/RowID)
  14595. TSQLRecordTableDeleted = class(TSQLRecord)
  14596. protected
  14597. fDeleted: Int64;
  14598. published
  14599. /// this Deleted published field will track the deleted row
  14600. // - defined as Int64 and not TID, to avoid the generation of the index on
  14601. // this column, which is not needed here (all requests are about ID/RowID)
  14602. property Deleted: Int64 read fDeleted write fDeleted;
  14603. end;
  14604. /// class-reference type (metaclass) to specify the storage table to be used
  14605. // for tracking TSQLRecord deletion
  14606. TSQLRecordTableDeletedClass = class of TSQLRecordTableDeleted;
  14607. /// defines what is stored in a TSQLRestTempStorageItem entry
  14608. TSQLRestTempStorageItemKind = set of (itemInsert,itemFakeID);
  14609. /// used to store an entry in the TSQLRestTempStorage class
  14610. TSQLRestTempStorageItem = record
  14611. /// the ID of this entry
  14612. // - after an AddCopy(ForceID=false), is a "fake" ID, which is > maxInt
  14613. ID: TID;
  14614. /// the stored item, either after adding or updating
  14615. // - equals nil if the item has been deleted
  14616. Value: TSQLRecord;
  14617. /// identify the fields stored in the Value instance
  14618. // - e.g. an Update() - or even an Add() - may only have set only simple or
  14619. // specific fields
  14620. ValueFields: TSQLFieldBits;
  14621. /// what is stored in this entry
  14622. Kind: TSQLRestTempStorageItemKind;
  14623. end;
  14624. /// used to store the entries in the TSQLRestTempStorage class
  14625. TSQLRestTempStorageItemDynArray = array of TSQLRestTempStorageITem;
  14626. /// abstract class used for temporary in-memory storage of TSQLRecord
  14627. // - purpose of this class is to gather write operations (Add/Update/Delete)
  14628. // - inherited implementations may send all updates at once to a server (i.e.
  14629. // "asynchronous write"), or maintain a versioned image of the content
  14630. // - all public methods (AddCopy/AddOwned/Update/Delete/FlushAsBatch) are
  14631. // thread-safe, protected by a mutex lock
  14632. TSQLRestTempStorage = class(TSynPersistentLocked)
  14633. protected
  14634. fStoredClass: TSQLRecordClass;
  14635. fStoredClassRecordProps: TSQLRecordProperties;
  14636. fItem: TSQLRestTempStorageItemDynArray;
  14637. fItems: TDynArray;
  14638. fLastFakeID: TID;
  14639. fCount: integer;
  14640. function InternalSetFields(const FieldNames: RawUTF8; out Fields: TSQLFieldBits): boolean;
  14641. procedure InternalAddItem(const item: TSQLRestTempStorageItem);
  14642. public
  14643. /// initialize the temporary storage for a given class
  14644. constructor Create(aClass: TSQLRecordClass); reintroduce; virtual;
  14645. /// finalize this temporary storage instance
  14646. destructor Destroy; override;
  14647. /// add a copy of a TSQLRecord to the internal storage list
  14648. // - if ForceID is true, Value.ID would be supplied with the ID to add
  14649. // - if ForceID is false, a "fake" ID is returned, which may be used later
  14650. // on for Update() calls - WARNING: but this ID should not be stored as
  14651. // a cross reference in another record, since it is private to this storage;
  14652. // the definitive ID will be returned eventually after proper persistence
  14653. // (e.g. sent as TSQLRestBatch to a mORMot server)
  14654. // - FieldNames can be the CSV list of field names to be set
  14655. // - if FieldNames is '', will set all simple fields, excluding BLOBs
  14656. // - if FieldNames is '*', will set ALL fields, including BLOBs
  14657. // - this method will clone the supplied Value, and make its own copy
  14658. // for its internal storage - consider use AddOwned() if the caller does
  14659. // not need to store the instance afterwards
  14660. function AddCopy(Value: TSQLRecord; ForceID: boolean;
  14661. const FieldNames: RawUTF8=''): TID; overload;
  14662. /// add and own a TSQLRecord in the internal storage list
  14663. // - if ForceID is true, Value.ID would be supplied with the ID to add
  14664. // - if ForceID is false, a "fake" ID is returned, which may be used later
  14665. // on for Update() calls - WARNING: but this ID should not be stored as
  14666. // a cross reference in another record, since it is private to this storage;
  14667. // the definitive ID will be returned eventually after proper persistence
  14668. // (e.g. sent as TSQLRestBatch to a mORMot server)
  14669. // - FieldNames can be the CSV list of field names to be set
  14670. // - if FieldNames is '', will set all simple fields, excluding BLOBs
  14671. // - if FieldNames is '*', will set ALL fields, including BLOBs
  14672. // - this method will store the supplied Value, and let its internal
  14673. // storage owns it and manage its lifetime - consider use AddCopy() if the
  14674. // caller does need to store this instance afterwards
  14675. // - returns 0 in case of error (e.g. ForceID and no or duplicated Value.ID)
  14676. function AddOwned(Value: TSQLRecord; ForceID: boolean;
  14677. const FieldNames: RawUTF8=''): TID; overload;
  14678. /// add, update or delete a TSQLRecord in the internal storage list
  14679. // - could be used from a TNotifySQLEvent/InternalUpdateEvent(seAdd) callback
  14680. // - here the value to be added is supplied as a JSON object and a ID field
  14681. // - returns false in case of error (e.g. duplicated ID or void JSON)
  14682. function FromEvent(Event: TSQLEvent; ID: TID; const JSON: RawUTF8): boolean;
  14683. /// add and own a TSQLRecord in the internal storage list
  14684. // - if ForceID is true, Value.ID would be supplied with the ID to add
  14685. // - if ForceID is false, a "fake" ID is returned, which may be used later
  14686. // on for Update() calls - WARNING: but this ID should not be stored as
  14687. // a cross reference in another record, since it is private to this storage;
  14688. // the definitive ID will be returned eventually after proper persistence
  14689. // (e.g. sent as TSQLRestBatch to a mORMot server)
  14690. // - this overloaded version expects the fields to be specified as bits
  14691. // - this method will store the supplied Value, and let its internal
  14692. // storage owns it and manage its lifetime - consider use AddCopy() if the
  14693. // caller does need to store this instance afterwards
  14694. // - returns 0 in case of error (e.g. ForceID and no or duplicated Value.ID)
  14695. function AddOwned(Value: TSQLRecord; ForceID: boolean;
  14696. const Fields: TSQLFieldBits): TID; overload;
  14697. /// mark a TSQLRecord as deleted in the internal storage list
  14698. procedure Delete(const ID: TID);
  14699. /// update a TSQLRecord and store the new values in the internal storage list
  14700. // - Value.ID is used to identify the record to be updated (which may be
  14701. // a just added "fake" ID)
  14702. // - FieldNames can be the CSV list of field names to be updated
  14703. // - if FieldNames is '', will update all simple fields, excluding BLOBs
  14704. // - if FieldNames is '*', will update ALL fields, including BLOBs
  14705. // - the supplied Value won't be owned by this instance: the caller should
  14706. // release it when Value is no longer needed
  14707. // - returns false in case of error (e.g. unknwown ID or invalid fields)
  14708. function Update(Value: TSQLRecord; const FieldNames: RawUTF8=''): boolean; overload;
  14709. /// update a TSQLRecord and store the new values in the internal storage list
  14710. // - Value.ID is used to identify the record to be updated (which may be
  14711. // a just added "fake" ID)
  14712. // - this overloaded version expects the fields to be specified as bits
  14713. // - the supplied Value won't be owned by this instance: the caller should
  14714. // release it when Value is no longer needed
  14715. // - returns false in case of error (e.g. unknwown ID or no field set)
  14716. function Update(Value: TSQLRecord; const Fields: TSQLFieldBits): boolean; overload;
  14717. /// convert the internal list as a TSQLRestBatch instance, ready to be
  14718. // sent to the server
  14719. function FlushAsBatch(Rest: TSQLRest;
  14720. AutomaticTransactionPerRow: cardinal=1000): TSQLRestBatch;
  14721. /// direct access to the low-level storage list
  14722. // - the Count property is the number of items, length(Item) is the capacity
  14723. // - the list is stored in increasing ID order
  14724. property Item: TSQLRestTempStorageItemDynArray read fItem;
  14725. /// how many entries are stored in the low-level storage list
  14726. property Count: integer read fCount;
  14727. end;
  14728. /// how TSQLRestServer should maintain its statistical information
  14729. // - used by TSQLRestServer.StatLevels property
  14730. TSQLRestServerMonitorLevels = set of (
  14731. mlTables, mlMethods, mlInterfaces, mlSessions, mlSQLite3);
  14732. /// used for high-level statistics in TSQLRestServer.URI()
  14733. TSQLRestServerMonitor = class(TSynMonitorServer)
  14734. protected
  14735. fServer: TSQLRestServer;
  14736. fStartDate: RawUTF8;
  14737. fCurrentThreadCount: TSynMonitorOneCount;
  14738. fSuccess: TSynMonitorCount64;
  14739. fOutcomingFiles: TSynMonitorCount64;
  14740. fServiceMethod: TSynMonitorCount64;
  14741. fServiceInterface: TSynMonitorCount64;
  14742. fCreated: TSynMonitorCount64;
  14743. fRead: TSynMonitorCount64;
  14744. fUpdated: TSynMonitorCount64;
  14745. fDeleted: TSynMonitorCount64;
  14746. // [Write: boolean] per-table statistics
  14747. fPerTable: array[boolean] of TSynMonitorWithSizeObjArray;
  14748. // no overriden Changed: TSQLRestServer.URI would do it in finally block
  14749. public
  14750. /// initialize the instance
  14751. constructor Create(aServer: TSQLRestServer); reintroduce;
  14752. /// finalize the instance
  14753. destructor Destroy; override;
  14754. /// should be called when a task successfully ended
  14755. // - thread-safe method
  14756. procedure ProcessSuccess(IsOutcomingFile: boolean); virtual;
  14757. /// update and returns the CurrentThreadCount property
  14758. // - this method is thread-safe
  14759. function NotifyThreadCount(delta: integer): integer;
  14760. /// update the Created/Read/Updated/Deleted properties
  14761. // - this method is thread-safe
  14762. procedure NotifyORM(aMethod: TSQLURIMethod);
  14763. /// update the per-table statistics
  14764. // - this method is thread-safe
  14765. procedure NotifyORMTable(TableIndex, DataSize: integer; Write: boolean;
  14766. const MicroSecondsElapsed: QWord);
  14767. published
  14768. /// when this monitoring instance (therefore the server) was created
  14769. property StartDate: RawUTF8 read fStartDate;
  14770. /// number of valid responses
  14771. // - i.e. which returned status code 200/HTML_SUCCESS or 201/HTML_CREATED
  14772. // - any invalid request will increase the TSynMonitor.Errors property
  14773. property Success: TSynMonitorCount64 read fSuccess;
  14774. /// count of the remote method-based service calls
  14775. property ServiceMethod: TSynMonitorCount64 read fServiceMethod;
  14776. /// count of the remote interface-based service calls
  14777. property ServiceInterface: TSynMonitorCount64 read fServiceInterface;
  14778. /// count of files transmitted directly (not part of Output size property)
  14779. // - i.e. when the service uses STATICFILE_CONTENT_TYPE/HTTP_RESP_STATICFILE
  14780. // as content type to let the HTTP server directly serve the file content
  14781. property OutcomingFiles: TSynMonitorCount64 read fOutcomingFiles;
  14782. /// number of current declared thread counts
  14783. // - as registered by BeginCurrentThread/EndCurrentThread
  14784. property CurrentThreadCount: TSynMonitorOneCount read fCurrentThreadCount;
  14785. /// how many Create / Add ORM operations did take place
  14786. property Created: TSynMonitorCount64 read fCreated;
  14787. /// how many Read / Get ORM operations did take place
  14788. property Read: TSynMonitorCount64 read fRead;
  14789. /// how many Update ORM operations did take place
  14790. property Updated: TSynMonitorCount64 read fUpdated;
  14791. /// how many Delete ORM operations did take place
  14792. property Deleted: TSynMonitorCount64 read fDeleted;
  14793. end;
  14794. /// ORM table used to store TSynMonitorUsage information
  14795. // - the ID primary field is the TSynMonitorUsageID shifted by 16 bits
  14796. TSQLMonitorUsage = class(TSQLRecord)
  14797. protected
  14798. fGran: TSynMonitorUsageGranularity;
  14799. fProcess: TSynUniqueIdentifierProcess;
  14800. fInfo: variant;
  14801. fComment: RawUTF8;
  14802. function GetUsageID: integer;
  14803. procedure SetUsageID(Value: integer);
  14804. public
  14805. /// compute the corresponding TSynMonitorUsageID.Value
  14806. // - according to the stored Process field
  14807. property UsageID: integer read GetUsageID write SetUsageID;
  14808. published
  14809. /// the granularity of the statistics of this entry
  14810. property Gran: TSynMonitorUsageGranularity read fGran write fGran;
  14811. /// identify which application is monitored
  14812. property Process: TSynUniqueIdentifierProcess read fProcess write fProcess;
  14813. /// the actual statistics information, stored as a TDocVariant JSON object
  14814. property Info: variant read fInfo write fInfo;
  14815. /// a custom text, which may be used e.g. by support or developpers
  14816. property Comment: RawUTF8 read fComment write fComment;
  14817. end;
  14818. /// class-reference type (metaclass) of a TSQLMonitorUsage table
  14819. TSQLMonitorUsageClass = class of TSQLMonitorUsage;
  14820. /// would store TSynMonitorUsage information in TSQLMonitorUsage ORM tables
  14821. // - the TSQLRecord.ID would be the TSynMonitorUsageID shifted by 16 bits
  14822. TSynMonitorUsageRest = class(TSynMonitorUsage)
  14823. private
  14824. protected
  14825. fStorage: TSQLRest;
  14826. fProcessID: TSynUniqueIdentifierProcess;
  14827. fStoredClass: TSQLMonitorUsageClass;
  14828. fStoredCache: array[mugHour..mugYear] of TSQLMonitorUsage;
  14829. function SaveDB(ID: integer; const Track: variant; Gran: TSynMonitorUsageGranularity): boolean; override;
  14830. function LoadDB(ID: integer; Gran: TSynMonitorUsageGranularity; out Track: variant): boolean; override;
  14831. public
  14832. /// initialize storage via ORM
  14833. // - if a TSynUniqueIdentifierProcess is supplied, it would be used to
  14834. // identify the generating process by shifting TSynMonitorUsageID values
  14835. // - would use TSQLMonitorUsage table, unless another one is specified
  14836. constructor Create(aStorage: TSQLRest; aProcessID: TSynUniqueIdentifierProcess;
  14837. aStoredClass: TSQLMonitorUsageClass=nil); reintroduce; virtual;
  14838. /// finalize the process
  14839. destructor Destroy; override;
  14840. published
  14841. /// the actual ORM class used for persistence
  14842. property StoredClass: TSQLMonitorUsageClass read fStoredClass;
  14843. /// how the information could be stored for several processes
  14844. // - e.g. when several SOA nodes gather monitoring information in a
  14845. // shared (MongoDB) database
  14846. property ProcessID: TSynUniqueIdentifierProcess read fProcessID;
  14847. end;
  14848. /// a specialized UTF-8 string type, used for TSQLRestServerURI storage
  14849. // - URI format is 'address:port/root', but port or root are optional
  14850. // - you could use TSQLRestServerURI record to store and process it
  14851. TSQLRestServerURIString = type RawUTF8;
  14852. /// a list of UTF-8 strings, used for TSQLRestServerURI storage
  14853. // - URI format is 'address:port/root', but port or root are optional
  14854. // - you could use TSQLRestServerURI record to store and process each item
  14855. TSQLRestServerURIStringDynArray = array of TSQLRestServerURIString;
  14856. /// used to access a TSQLRestServer from its TSQLRestServerURIString URI
  14857. // - URI format is 'address:port/root', and may be transmitted as
  14858. // TSQLRestServerURIString text instances
  14859. {$ifndef ISDELPHI2010}
  14860. TSQLRestServerURI = object
  14861. {$else}
  14862. TSQLRestServerURI = record
  14863. {$endif}
  14864. private
  14865. function GetURI: TSQLRestServerURIString;
  14866. procedure SetURI(const Value: TSQLRestServerURIString);
  14867. public
  14868. /// the TSQLRestServer IP Address or DNS name
  14869. Address: RawUTF8;
  14870. /// the TSQLRestServer IP port
  14871. Port: RawUTF8;
  14872. /// the TSQLRestServer model Root
  14873. Root: RawUTF8;
  14874. /// returns TRUE if all field values do match, case insensitively
  14875. function Equals(const other: TSQLRestServerURI): boolean;
  14876. /// property which allows to read or set the Address/Port/Root fields as
  14877. // one UTF-8 text field (i.e. a TSQLRestServerURIString instance)
  14878. // - URI format is 'address:port/root', but port or root are optional
  14879. property URI: TSQLRestServerURIString read GetURI write SetURI;
  14880. end;
  14881. /// store a list of TSQLRestServer URIs
  14882. TSQLRestServerURIDynArray = array of TSQLRestServerURI;
  14883. /// used to publish all Services supported by a TSQLRestServer instance
  14884. // - as expected by TSQLRestServer.ServicesPublishedInterfaces
  14885. // - can be serialized as a JSON object via RecordLoadJSON/RecordSaveJSON
  14886. TServicesPublishedInterfaces = object
  14887. /// how this TSQLRestServer could be accessed
  14888. PublicURI: TSQLRestServerURI;
  14889. /// the list of supported services names
  14890. // - in fact this is the Interface name without the initial 'I', e.g.
  14891. // 'Calculator' for ICalculator
  14892. Names: TRawUTF8DynArray;
  14893. end;
  14894. /// store a list of published Services supported by a TSQLRestServer instance
  14895. TServicesPublishedInterfacesDynArray = array of TServicesPublishedInterfaces;
  14896. /// used e.g. by TSQLRestServer to store a list of TServicesPublishedInterfaces
  14897. TServicesPublishedInterfacesList = class(TSynPersistentLocked)
  14898. private
  14899. fDynArray: TDynArray;
  14900. fDynArrayTimeoutTix: TDynArray;
  14901. fTimeoutTix: TInt64DynArray;
  14902. fTimeoutTixCount: integer;
  14903. fLastPublishedJson: cardinal;
  14904. fTimeOut: integer;
  14905. public
  14906. /// the internal list of published services
  14907. // - the list is stored in-order, i.e. it will follow the RegisterFromJSON()
  14908. // execution order: the latest registrations would appear last
  14909. List: TServicesPublishedInterfacesDynArray;
  14910. /// how many items are actually stored in List[]
  14911. Count: Integer;
  14912. /// initialize the storage
  14913. // - an optional time out period, in milliseconds, may be defined - but the
  14914. // clients should ensure that RegisterFromClientJSON() is called in order
  14915. // to refresh the list (e.g. from _contract_ HTTP body)
  14916. constructor Create(aTimeoutMS: integer); reintroduce; virtual;
  14917. /// add the JSON serialized TServicesPublishedInterfaces to the list
  14918. // - called by TSQLRestServerURIContext.InternalExecuteSOAByInterface when
  14919. // the client provides its own services as _contract_ HTTP body
  14920. // - warning: supplied PublishedJson would be parsed in place, so modified
  14921. procedure RegisterFromClientJSON(var PublishedJson: RawUTF8);
  14922. /// set the list from JSON serialized TServicesPublishedInterfacesDynArray
  14923. // - may be used to duplicate the whole TSQLRestServer.AssociatedServices
  14924. // content, as returned from /root/Stat?findservice=*
  14925. // - warning: supplied PublishedJson would be parsed in place, so modified
  14926. procedure RegisterFromServerJSON(var PublishedJson: RawUTF8);
  14927. /// set the list from a remote TSQLRestServer
  14928. // - will call /root/Stat?findservice=* URI, then RegisterFromServerJSON()
  14929. function RegisterFromServer(Client: TSQLRestClientURI): boolean;
  14930. /// search for a public URI in the registration list
  14931. function FindURI(const aPublicURI: TSQLRestServerURI): integer;
  14932. /// search for the latest registrations of a service, by name
  14933. // - will lookup for the Interface name without the initial 'I', e.g.
  14934. // 'Calculator' for ICalculator - warning: research is case-sensitive
  14935. // - if the service name has been registered several times, all
  14936. // registration would be returned, the latest in first position
  14937. function FindService(const aServiceName: RawUTF8): TSQLRestServerURIDynArray;
  14938. /// return all services URI by name, from the registration list, as URIs
  14939. // - will lookup for the Interface name without the initial 'I', e.g.
  14940. // 'Calculator' for ICalculator - warning: research is case-sensitive
  14941. // - the returned string would contain all matching server URI, the latest
  14942. // registration being the first to appear, e.g.
  14943. // $ ["addresslast:port/root","addressprevious:port/root","addressfirst:port/root"]
  14944. function FindServiceAll(const aServiceName: RawUTF8): TSQLRestServerURIStringDynArray; overload;
  14945. /// return all services URI by name, from the registration list, as JSON
  14946. // - will lookup for the Interface name without the initial 'I', e.g.
  14947. // 'Calculator' for ICalculator - warning: research is case-sensitive
  14948. // - the returned JSON array would contain all matching server URI, encoded as
  14949. // a TSQLRestServerURI JSON array, the latest registration being
  14950. // the first to appear, e.g.
  14951. // $ [{"Address":"addresslast","Port":"port","Root":"root"},...]
  14952. // - if aServiceName='*', it will return ALL registration items, encoded as
  14953. // a TServicesPublishedInterfaces JSON array, e.g.
  14954. // $ [{"PublicURI":{"Address":"1.2.3.4","Port":"123","Root":"root"},"Names":['Calculator']},...]
  14955. procedure FindServiceAll(const aServiceName: RawUTF8; aWriter: TTextWriter); overload;
  14956. /// the number of milliseconds after which an entry expires
  14957. // - is 0 by default, meaning no expiration
  14958. // - you can set it to a value so that any service URI registered with
  14959. // RegisterFromJSON() AFTER this property modification may expire
  14960. property TimeOut: integer read fTimeOut write fTimeOut;
  14961. end;
  14962. /// class-reference type (metaclass) of a REST server
  14963. TSQLRestServerClass = class of TSQLRestServer;
  14964. /// some options for TSQLRestServer process
  14965. // - read-only rsoNoAJAXJSON indicates that JSON data is transmitted in "not
  14966. // expanded" format: you should NEVER change this option by including
  14967. // this property in TSQLRestServer.Options, but always call explicitly
  14968. // TSQLRestServer.NoAJAXJSON := true so that the SetNoAJAXJSON virtual
  14969. // method should be called as expected (e.g. to flush TSQLRestServerDB cache)
  14970. // - rsoGetAsJsonNotAsString would let ORM GET return to AJAX (non Delphi)
  14971. // clients JSON objects instead of the JSON text stored in database fields
  14972. // - rsoGetID_str would add a "ID_str": string field to circumvent JavaScript
  14973. // limitation of 53-bit for integers - only for AJAX (non Delphi) clients
  14974. // - unauthenticated requests from browsers (i.e. not Delphi clients) may
  14975. // be redirected to the TSQLRestServer.Auth() method via rsoRedirectForbiddenToAuth
  14976. // (e.g. for TSQLRestServerAuthenticationHttpBasic popup)
  14977. // - some REST/AJAX clients may expect to return status code 204 as
  14978. // instead of 200 in case of a successful operation, but with no returned
  14979. // body (e.g. a DELETE with SAPUI5 / OpenUI5 framework): include
  14980. // rsoHtml200WithNoBodyReturns204 so that any HTML_SUCCESS (200) with no
  14981. // returned body would return a HTML_NOCONTENT (204)
  14982. // - by default, Add() or Update() would return HTML_CREATED (201) or
  14983. // HTML_SUCCESS (200) with no body, unless rsoAddUpdateReturnsContent is set
  14984. // to return as JSON the last inserted/updated record
  14985. // - TModTime / TCreateTime fields are expected to be filled on client side,
  14986. // unless you set rsoComputeFieldsBeforeWriteOnServerSide so that AJAX requests
  14987. // would set the fields on the server side by calling the TSQLRecord
  14988. // ComputeFieldsBeforeWrite virtual method, before writing to the database
  14989. TSQLRestServerOption = (
  14990. rsoNoAJAXJSON,
  14991. rsoGetAsJsonNotAsString,
  14992. rsoGetID_str,
  14993. rsoRedirectForbiddenToAuth,
  14994. rsoHtml200WithNoBodyReturns204,
  14995. rsoAddUpdateReturnsContent,
  14996. rsoComputeFieldsBeforeWriteOnServerSide);
  14997. /// allow to customize the TSQLRestServer process via its Options property
  14998. TSQLRestServerOptions = set of TSQLRestServerOption;
  14999. /// a generic REpresentational State Transfer (REST) server
  15000. // - descendent must implement the protected EngineList() Retrieve() Add()
  15001. // Update() Delete() methods
  15002. // - automatic call of this methods by a generic URI() RESTful function
  15003. // - any published method of descendants must match TSQLRestServerCallBack
  15004. // prototype, and is expected to be thread-safe
  15005. TSQLRestServer = class(TSQLRest)
  15006. protected
  15007. fVirtualTableDirect: boolean;
  15008. fHandleAuthentication: boolean;
  15009. fBypassORMAuthentication: TSQLURIMethods;
  15010. fAfterCreation: boolean;
  15011. fOptions: TSQLRestServerOptions;
  15012. /// the TSQLAuthUser and TSQLAuthGroup classes, as defined in model
  15013. fSQLAuthUserClass: TSQLAuthUserClass;
  15014. fSQLAuthGroupClass: TSQLAuthGroupClass;
  15015. /// how in-memory sessions are handled
  15016. fSessionClass: TAuthSessionClass;
  15017. /// will contain the in-memory representation of some static tables
  15018. // - this array has the same length as the associated Model.Tables[]
  15019. // - fStaticData[] will contain pure in-memory tables, not declared as
  15020. // SQLite3 virtual tables, therefore not available from joined SQL statements
  15021. fStaticData: TSQLRestDynArray;
  15022. /// map TSQLRestStorageInMemory or TSQLRestStorageExternal engines
  15023. // - this array has the same length as the associated Model.Tables[]
  15024. // - fStaticVirtualTable[] will contain in-memory or external tables declared
  15025. // as SQLite3 virtual tables, therefore available from joined SQL statements
  15026. fStaticVirtualTable: TSQLRestDynArray;
  15027. /// in-memory storage of TAuthSession instances
  15028. fSessions: TObjectListLocked;
  15029. /// used to compute genuine TAuthSession.ID cardinal value
  15030. fSessionCounter: cardinal;
  15031. fSessionAuthentication: TSQLRestServerAuthenticationDynArray;
  15032. {$ifdef MSWINDOWS}
  15033. /// thread initialized by ExportServerNamedPipe() to response to client through a pipe
  15034. fExportServerNamedPipeThread: TSQLRestServerNamedPipe;
  15035. /// internal server window handle, initialized by ExportServerMessage() method
  15036. fServerWindow: HWND;
  15037. /// internal server window class name, initialized by ExportServerMessage() method
  15038. // - use "string" type, i.e. UnicodeString for Delphi 2009+, in order
  15039. // to call directly the correct FindWindow?()=FindWindow Win32 API
  15040. fServerWindowName: string;
  15041. {$endif}
  15042. fPublishedMethod: TSQLRestServerMethods;
  15043. fPublishedMethods: TDynArrayHashed;
  15044. fPublishedMethodBatchIndex: integer;
  15045. fPublicURI: TSQLRestServerURI;
  15046. fAssociatedServices: TServicesPublishedInterfacesList;
  15047. fStats: TSQLRestServerMonitor;
  15048. fStatLevels: TSQLRestServerMonitorLevels;
  15049. fStatUsage: TSynMonitorUsage;
  15050. fShutdownRequested: boolean;
  15051. fCreateMissingTablesOptions: TSQLInitializeTableOptions;
  15052. fRootRedirectGet: RawUTF8;
  15053. fRecordVersionMax: TRecordVersion;
  15054. fRecordVersionDeleteIgnore: boolean;
  15055. fOnIdleLastTix: cardinal;
  15056. fSQLRecordVersionDeleteTable: TSQLRecordTableDeletedClass;
  15057. fRecordVersionSlaveCallbacks: array of IServiceRecordVersionCallback;
  15058. // TSQLRecordHistory.ModifiedRecord handles up to 64 (=1 shl 6) tables
  15059. fTrackChangesHistoryTableIndex: TIntegerDynArray;
  15060. fTrackChangesHistoryTableIndexCount: cardinal;
  15061. fTrackChangesHistory: array of record
  15062. CurrentRow: integer;
  15063. MaxSentDataJsonRow: integer;
  15064. MaxRevisionJSON: integer;
  15065. MaxUncompressedBlobSize: integer;
  15066. end;
  15067. constructor RegisteredClassCreateFrom(aModel: TSQLModel;
  15068. aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); reintroduce; virtual;
  15069. function GetAuthenticationSchemesCount: integer;
  15070. function GetCurrentSessionUserID: TID; override;
  15071. // called by Stat() and Info() method-based services
  15072. procedure InternalStat(Ctxt: TSQLRestServerURIContext; W: TTextWriter); virtual;
  15073. procedure InternalInfo(var info: TDocVariantData); virtual;
  15074. procedure SetStatUsage(usage: TSynMonitorUsage);
  15075. function GetServiceMethodStat(const aMethod: RawUTF8): TSynMonitorInputOutput;
  15076. /// fast get the associated static server, if any
  15077. function GetStaticDataServer(aClass: TSQLRecordClass): TSQLRest;
  15078. /// retrieve a TSQLRestStorage instance associated to a Virtual Table
  15079. // - is e.g. TSQLRestStorageInMemory instance associated to a
  15080. // TSQLVirtualTableBinary or TSQLVirtualTableJSON class
  15081. // - may be a TSQLRestStorageExternal (as defined in mORMotDB unit)
  15082. // for a virtual table giving access to an external database
  15083. function GetVirtualTable(aClass: TSQLRecordClass): TSQLRest;
  15084. /// fast get the associated static server or Virtual table, if any
  15085. // - this can be used to call directly the TSQLRestStorage instance
  15086. // on the server side
  15087. // - same as a dual call to StaticDataServer[aClass] + StaticVirtualTable[aClass]
  15088. // - TSQLRestServer.URI will make a difference between the a static server
  15089. // or a TSQLVirtualTable, but this method won't - you can set a reference
  15090. // to a TSQLRestServerKind variable to retrieve the database server type
  15091. function GetStaticDataServerOrVirtualTable(aClass: TSQLRecordClass): TSQLRest; overload;
  15092. {$ifdef HASINLINE}inline;{$endif}
  15093. /// overloaded method using table index in associated Model
  15094. function GetStaticDataServerOrVirtualTable(aTableIndex: integer): TSQLRest;
  15095. overload; {$ifdef HASINLINE}inline;{$endif}
  15096. function GetStaticDataServerOrVirtualTable(aTableIndex: integer;
  15097. out Kind: TSQLRestServerKind): TSQLRest; overload;
  15098. {$ifdef HASINLINE}inline;{$endif}
  15099. function GetRemoteTable(TableIndex: Integer): TSQLRest;
  15100. function IsInternalSQLite3Table(aTableIndex: integer): boolean;
  15101. /// retrieve a list of members as JSON encoded data - used by OneFieldValue()
  15102. // and MultiFieldValue() public functions
  15103. function InternalAdaptSQL(TableIndex: integer; var SQL: RawUTF8): TSQLRest;
  15104. function InternalListRawUTF8(TableIndex: integer; const SQL: RawUTF8): RawUTF8;
  15105. /// will retrieve the monotonic value of a TRecordVersion field from the DB
  15106. procedure InternalRecordVersionMaxFromExisting(RetrieveNext: PID); virtual;
  15107. procedure InternalRecordVersionDelete(TableIndex: integer; ID: TID;
  15108. Batch: TSQLRestBatch); virtual;
  15109. procedure InternalRecordVersionHandle(Occasion: TSQLOccasion;
  15110. TableIndex: integer; var Decoder: TJSONObjectDecoder;
  15111. RecordVersionField: TSQLPropInfoRTTIRecordVersion); virtual;
  15112. /// will compute the next monotonic value for a TRecordVersion field
  15113. // - you may override this method to customize the returned Int64 value
  15114. // (e.g. to support several synchronization nodes)
  15115. function InternalRecordVersionComputeNext: TRecordVersion; virtual;
  15116. /// this method is overridden for setting the NoAJAXJSON field
  15117. // of all associated TSQLRestStorage servers
  15118. procedure SetNoAJAXJSON(const Value: boolean); virtual;
  15119. function GetNoAJAXJSON: boolean;
  15120. /// add a new session to the internal session list
  15121. // - do not use this method directly: this callback is to be used by
  15122. // TSQLRestServerAuthentication* classes
  15123. // - will check that the logon name is valid
  15124. // - on failure, will call TSQLRestServerURIContext.AuthenticationFailed()
  15125. // with afSessionAlreadyStartedForThisUser or afSessionCreationAborted reason
  15126. procedure SessionCreate(var User: TSQLAuthUser; Ctxt: TSQLRestServerURIContext;
  15127. out Session: TAuthSession); virtual;
  15128. /// fill the supplied context from the supplied aContext.Session ID
  15129. // - returns nil if not found, or fill aContext.User/Group values if matchs
  15130. // - this method will also check for outdated sessions, and delete them
  15131. // - this method is not thread-safe: caller should use fSessions.Lock
  15132. function SessionAccess(Ctxt: TSQLRestServerURIContext): TAuthSession;
  15133. /// delete a session from its index in fSessions[]
  15134. // - will perform any needed clean-up, and log the event
  15135. // - this method is not thread-safe: caller should use fSessions.Lock
  15136. procedure SessionDelete(aSessionIndex: integer; Ctxt: TSQLRestServerURIContext);
  15137. /// returns TRUE if this table is worth caching (e.g. already in memory)
  15138. // - this overridden implementation returns FALSE for TSQLRestStorageInMemory
  15139. function CacheWorthItForTable(aTableIndex: cardinal): boolean; override;
  15140. /// overridden methods which will perform CRUD operations
  15141. // - will call any static TSQLRestStorage, or call MainEngine*() virtual methods
  15142. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  15143. function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
  15144. function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
  15145. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  15146. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  15147. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  15148. const IDs: TIDDynArray): boolean; override;
  15149. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  15150. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  15151. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  15152. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  15153. function EngineUpdateField(TableModelIndex: integer;
  15154. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  15155. function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  15156. const FieldName: RawUTF8; Increment: Int64): boolean; override;
  15157. function EngineBatchSend(Table: TSQLRecordClass; const Data: RawUTF8;
  15158. var Results: TIDDynArray; ExpectedResultsCount: integer): integer; override;
  15159. /// virtual methods which will perform CRUD operations on the main DB
  15160. function MainEngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; virtual; abstract;
  15161. function MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; virtual; abstract;
  15162. function MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; virtual; abstract;
  15163. function MainEngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; virtual; abstract;
  15164. function MainEngineDelete(TableModelIndex: integer; ID: TID): boolean; virtual; abstract;
  15165. function MainEngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  15166. const IDs: TIDDynArray): boolean; virtual; abstract;
  15167. function MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  15168. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; virtual; abstract;
  15169. function MainEngineUpdateBlob(TableModelIndex: integer; aID: TID;
  15170. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; virtual; abstract;
  15171. function MainEngineUpdateField(TableModelIndex: integer;
  15172. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; virtual; abstract;
  15173. function MainEngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  15174. const FieldName: RawUTF8; Increment: Int64): boolean; virtual; abstract;
  15175. public
  15176. /// this integer property is incremented by the database engine when any SQL
  15177. // statement changes the database contents (i.e. on any not SELECT statement)
  15178. // - its value can be published to the client on every remote request
  15179. // - it may be used by client to avoid retrieve data only if necessary
  15180. // - if its value is 0, this feature is not activated on the server, and the
  15181. // client must ignore it and always retrieve the content
  15182. InternalState: Cardinal;
  15183. /// a method can be specified here to trigger events after any table update
  15184. // - is called BEFORE deletion, and AFTER insertion or update
  15185. // - note that the aSentData parameter does not contain all record fields,
  15186. // but only transmitted information: e.g. if only one field is updated, only
  15187. // this single field (and the ID) is available
  15188. // - to be used only server-side, not to synchronize some clients: the framework
  15189. // is designed around a stateless RESTful architecture (like HTTP/1.1), in which
  15190. // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer)
  15191. OnUpdateEvent: TNotifySQLEvent;
  15192. /// a method can be specified here to trigger events after any blob update
  15193. // - is called AFTER update of one or several blobs, never on delete nor insert
  15194. // - to be used only server-side, not to synchronize some clients: the framework
  15195. // is designed around a stateless RESTful architecture (like HTTP/1.1), in which
  15196. // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer)
  15197. OnBlobUpdateEvent: TNotifyFieldSQLEvent;
  15198. /// a method can be specified to be notified when a session is created
  15199. // - for OnSessionCreate, returning TRUE will abort the session creation -
  15200. // and you can set Ctxt.Call^.OutStatus to a corresponding error code
  15201. // - it could be used e.g. to limit the number of client sessions
  15202. OnSessionCreate: TNotifySQLSession;
  15203. /// a custom method to retrieve the TSQLAuthUser instance for authentication
  15204. // - will be called by TSQLRestServerAuthentication.GetUser() instead of
  15205. // plain SQLAuthUserClass.Create()
  15206. OnAuthenticationUserRetrieve: TOnAuthenticationUserRetrieve;
  15207. /// this event handler will be executed when a session failed to initialize
  15208. // (DenyOfService attack?) or the request is not valid (ManIntheMiddle attack?)
  15209. // - e.g. if the URI signature is invalid, or OnSessionCreate event handler
  15210. // aborted the session creation by returning TRUE (in this later case,
  15211. // the Session parameter is not nil)
  15212. // - you can access the current execution context from the Ctxt parameter,
  15213. // e.g. to retrieve the caller's IP and ban aggressive users:
  15214. // ! FindIniNameValue(pointer(Ctxt.Call^.InHead),'REMOTEIP: ')
  15215. OnAuthenticationFailed: TNotifyAuthenticationFailed;
  15216. /// a method can be specified to be notified when a session is closed
  15217. // - for OnSessionClosed, the returning boolean value is ignored
  15218. // - Ctxt is nil if the session is closed due to a timeout
  15219. // - Ctxt is not nil if the session is closed explicitly by the client
  15220. OnSessionClosed: TNotifySQLSession;
  15221. /// this event will be executed to push notifications from the server to
  15222. // a remote client, using a (fake) interface parameter
  15223. // - is nil by default, but may point e.g. to TSQLHttpServer.NotifyCallback
  15224. OnNotifyCallback: TSQLRestServerNotifyCallback;
  15225. /// this event will be executed by TServiceFactoryServer.CreateInstance
  15226. // - you may set a callback to customize a server-side service instance,
  15227. // i.e. inject class-level dependencies:
  15228. // !procedure TMyClass.OnCreateInstance(
  15229. // ! Sender: TServiceFactoryServer; Instance: TInterfacedObject);
  15230. // !begin
  15231. // ! if Sender.ImplementationClass=TLegacyStockQuery then
  15232. // ! TLegacyStockQuery(Instance).fDbConnection := fDbConnection;
  15233. // !end;
  15234. // - consider using a TInjectableObjectClass implementation for pure IoC/DI
  15235. OnServiceCreateInstance: TOnServiceCreateInstance;
  15236. /// event trigerred when URI() starts to process a request
  15237. // - the supplied Ctxt parameter would give access to the command about to
  15238. // be executed, e.g. Ctxt.Command=execSOAByInterface would identify a SOA
  15239. // service execution, with the corresponding Service and ServiceMethodIndex
  15240. // parameters as set by TSQLRestServerURIContext.URIDecodeSOAByInterface
  15241. // - should return TRUE if the method can be executed
  15242. // - should return FALSE if the method should not be executed, and the
  15243. // callback should set the corresponding error to the supplied context e.g.
  15244. // ! Ctxt.Error('Unauthorized method',HTML_NOTALLOWED);
  15245. // - since this event would be executed by every TSQLRestServer.URI call,
  15246. // it should better not make any slow process (like writing to a remote DB)
  15247. OnBeforeURI: TNotifyBeforeURI;
  15248. /// event trigerred when URI() finished to process a request
  15249. // - the supplied Ctxt parameter would give access to the command which has
  15250. // been executed, e.g. via Ctxt.Call.OutStatus or Ctxt.MicroSecondsElapsed
  15251. // - since this event would be executed by every TSQLRestServer.URI call,
  15252. // it should better not make any slow process (like writing to a remote DB)
  15253. OnAfterURI: TNotifyAfterURI;
  15254. /// event trigerred when URI() failed to process a request
  15255. // - if Ctxt.ExecuteCommand raised an execption, this callback would be
  15256. // run with all neeed information
  15257. // - should return TRUE to execute Ctxt.Error(E,...), FALSE if returned
  15258. // content has already been set as expected by the client
  15259. OnErrorURI: TNotifyErrorURI;
  15260. /// event trigerred when URI() is called, and at least 128 ms is elapsed
  15261. // - could be used to execute some additional process after a period of time
  15262. // - note that if TSQLRestServer.URI is not called by any client, this
  15263. // callback won't be executed either
  15264. OnIdle: TNotifyEvent;
  15265. /// this property can be used to specify the URI parmeters to be used
  15266. // for query paging
  15267. // - is set by default to PAGINGPARAMETERS_YAHOO constant by
  15268. // TSQLRestServer.Create() constructor
  15269. URIPagingParameters: TSQLRestServerURIPagingParameters;
  15270. /// implement Server-Side TSQLRest deletion
  15271. // - uses internally EngineDelete() function for calling the database engine
  15272. // - call corresponding fStaticData[] if necessary
  15273. // - this record is also erased in all available TRecordReference properties
  15274. // in the database Model, for relational database coherency
  15275. function Delete(Table: TSQLRecordClass; ID: TID): boolean; override;
  15276. /// implement Server-Side TSQLRest deletion with a WHERE clause
  15277. // - will process all ORM-level validation, coherency checking and
  15278. // notifications together with a low-level SQL deletion work (if possible)
  15279. function Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean; override;
  15280. /// overridden method for direct static class call (if any)
  15281. function TableRowCount(Table: TSQLRecordClass): Int64; override;
  15282. /// overridden method for direct static class call (if any)
  15283. function TableHasRows(Table: TSQLRecordClass): boolean; override;
  15284. /// virtual method called when a record is updated
  15285. // - default implementation will call the OnUpdateEvent/OnBlobUpdateEvent
  15286. // methods, if defined
  15287. // - will also handle TSQLRecordHistory tables, as defined by TrackChanges()
  15288. // - returns true on success, false if an error occured (but action must continue)
  15289. // - you can override this method to implement a server-wide notification,
  15290. // but be aware it may be the first step to break the stateless architecture
  15291. // of the framework
  15292. function InternalUpdateEvent(aEvent: TSQLEvent; aTableIndex: integer; aID: TID;
  15293. const aSentData: RawUTF8; aIsBlobFields: PSQLFieldBits): boolean; virtual;
  15294. /// initialize change tracking for the given tables
  15295. // - by default, it will use the TSQLRecordHistory table to store the
  15296. // changes - you can specify a dedicated class as aTableHistory parameter
  15297. // - if aTableHistory is not already part of the TSQLModel, it will be added
  15298. // - note that this setting should be consistent in time: if you disable
  15299. // tracking for a while, or did not enable tracking before adding a record,
  15300. // then the content history won't be consistent (or disabled) for this record
  15301. // - at every change, aTableHistory.SentDataJSON records will be added, up
  15302. // to aMaxHistoryRowBeforeBlob items - then aTableHistory.History will store
  15303. // a compressed version of all previous changes
  15304. // - aMaxHistoryRowBeforeBlob is the maximum number of JSON rows per Table
  15305. // before compression into BLOB is triggerred
  15306. // - aMaxHistoryRowPerRecord is the maximum number of JSON rows per record,
  15307. // above which the versions will be compressed as BLOB
  15308. // - aMaxUncompressedBlobSize is the maximum BLOB size per record
  15309. // - you can specify aMaxHistoryRowBeforeBlob=0 to disable change tracking
  15310. // - you should call this method after the CreateMissingTables call
  15311. // - note that change tracking may slow down the writing process, and
  15312. // may increase storage space a lot (even if BLOB maximum size can be set),
  15313. // so should be defined only when necessary
  15314. procedure TrackChanges(const aTable: array of TSQLRecordClass;
  15315. aTableHistory: TSQLRecordHistoryClass=nil; aMaxHistoryRowBeforeBlob: integer=1000;
  15316. aMaxHistoryRowPerRecord: integer=10; aMaxUncompressedBlobSize: integer=64*1024); virtual;
  15317. /// force compression of all aTableHistory.SentDataJson into History BLOB
  15318. // - by default, this will take place in InternalUpdateEvent() when
  15319. // aMaxHistoryRowBeforeBlob - as set by TrackChanges() method - is reached
  15320. // - you can manually call this method to force History BLOB update, e.g.
  15321. // when the server is in Idle state, and ready for process
  15322. procedure TrackChangesFlush(aTableHistory: TSQLRecordHistoryClass); virtual;
  15323. /// check if OnUpdateEvent or change tracked has been defined for this table
  15324. // - is used internally e.g. by TSQLRestServerDB.MainEngineUpdateField to
  15325. // ensure that the updated ID fields will be computed as expected
  15326. function InternalUpdateEventNeeded(aTableIndex: integer): boolean;
  15327. /// will compute the next monotonic value for a TRecordVersion field
  15328. function RecordVersionCompute: TRecordVersion;
  15329. /// read only access to the current monotonic value for a TRecordVersion field
  15330. function RecordVersionCurrent: TRecordVersion;
  15331. /// synchronous master/slave replication from a slave TSQLRest
  15332. // - apply all the updates from another (distant) master TSQLRest for a given
  15333. // TSQLRecord table, using its TRecordVersion field, to the calling slave
  15334. // - both remote Master and local slave TSQLRestServer should have the supplied
  15335. // Table class in their data model (maybe in diverse order)
  15336. // - by default, all pending updates are retrieved, but you can define a value
  15337. // to ChunkRowLimit, so that the updates would be retrieved by smaller chunks
  15338. // - returns -1 on error, or the latest applied revision number (which may
  15339. // be 0 if there is no data in the table)
  15340. // - this method will use regular REST ORM commands, so will work with any
  15341. // communication channels: for real-time push synchronization, consider using
  15342. // RecordVersionSynchronizeMasterStart and RecordVersionSynchronizeSlaveStart
  15343. // over a bidirectionnal communication channel like WebSockets
  15344. // - you can use RecordVersionSynchronizeSlaveToBatch if your purpose is
  15345. // to access the updates before applying to the current slave storage
  15346. function RecordVersionSynchronizeSlave(Table: TSQLRecordClass;
  15347. Master: TSQLRest; ChunkRowLimit: integer=0; OnWrite: TOnBatchWrite=nil): TRecordVersion;
  15348. /// synchronous master/slave replication from a slave TSQLRest into a Batch
  15349. // - will retrieve all the updates from a (distant) master TSQLRest for a
  15350. // given TSQLRecord table, using its TRecordVersion field, and a supplied
  15351. // TRecordVersion monotonic value, into a TSQLRestBatch instance
  15352. // - both remote Source and local TSQLRestSever should have the supplied
  15353. // Table class in each of their data model
  15354. // - by default, all pending updates are retrieved, but you can define a value
  15355. // to MaxRowLimit, so that the updates would be retrieved by smaller chunks
  15356. // - returns nil if nothing new was found, or a TSQLRestBatch instance
  15357. // containing all modifications since RecordVersion revision
  15358. // - when executing the returned TSQLRestBatch on the database, you should
  15359. // set TSQLRestServer.RecordVersionDeleteIgnore := true so that the
  15360. // TRecordVersion fields would be forced from the supplied value
  15361. // - usually, you should not need to use this method, but rather the more
  15362. // straightforward RecordVersionSynchronizeSlave()
  15363. function RecordVersionSynchronizeSlaveToBatch(Table: TSQLRecordClass;
  15364. Master: TSQLRest; var RecordVersion: TRecordVersion;
  15365. MaxRowLimit: integer=0; OnWrite: TOnBatchWrite=nil): TSQLRestBatch; virtual;
  15366. /// initiate asynchronous master/slave replication on a master TSQLRest
  15367. // - allow synchronization of a TSQLRecord table, using its TRecordVersion
  15368. // field, for real-time master/slave replication on the master side
  15369. // - this method will register the IServiceRecordVersion service on the
  15370. // server side, so that RecordVersionSynchronizeStartSlave() would be able
  15371. // to receive push notifications of any updates
  15372. // - this method expects the communication channel to be bidirectional, e.g.
  15373. // a mORMotHTTPServer's TSQLHttpServer in useBidirSocket mode
  15374. function RecordVersionSynchronizeMasterStart(ByPassAuthentication: boolean=false): boolean;
  15375. /// initiate asynchronous master/slave replication on a slave TSQLRest
  15376. // - start synchronization of a TSQLRecord table, using its TRecordVersion
  15377. // field, for real-time master/slave replication on the slave side
  15378. // - this method will first retrieve any pending modification by regular
  15379. // REST calls to RecordVersionSynchronizeSlave, then create and register a
  15380. // callback instance using RecordVersionSynchronizeSubscribeMaster()
  15381. // - this method expects the communication channel to be bidirectional, e.g.
  15382. // a TSQLHttpClientWebsockets
  15383. // - the modifications will be pushed by the master, then applied to the
  15384. // slave storage, until RecordVersionSynchronizeSlaveStop method is called
  15385. // - an optional OnNotify event may be defined, which will be triggered
  15386. // for all incoming change, supllying the updated TSQLRecord instance
  15387. function RecordVersionSynchronizeSlaveStart(Table: TSQLRecordClass;
  15388. MasterRemoteAccess: TSQLRestClientURI; OnNotify: TOnBatchWrite=nil): boolean;
  15389. /// finalize asynchronous master/slave replication on a slave TSQLRest
  15390. // - stop synchronization of a TSQLRecord table, using its TRecordVersion
  15391. // field, for real-time master/slave replication on the slave side
  15392. // - expect a previous call to RecordVersionSynchronizeSlaveStart
  15393. function RecordVersionSynchronizeSlaveStop(Table: TSQLRecordClass): boolean;
  15394. /// low-level callback registration for asynchronous master/slave replication
  15395. // - you should not have to use this method, but rather
  15396. // RecordVersionSynchronizeMasterStart and RecordVersionSynchronizeSlaveStart
  15397. // RecordVersionSynchronizeSlaveStop methods
  15398. // - register a callback interface on the master side, which will be called
  15399. // each time a write operation is performed on a given TSQLRecord with a
  15400. // TRecordVersion field
  15401. // - the callback parameter could be a TServiceRecordVersionCallback instance,
  15402. // which would perform all update operations as expected
  15403. // - the callback process would be blocking for the ORM write point of view:
  15404. // so it should be as fast as possible, or asynchronous - note that regular
  15405. // callbacks using WebSockets, as implemented by SynBidirSock.pas and
  15406. // mORMotHTTPServer's TSQLHttpServer in useBidirSocket mode, are asynchronous
  15407. // - if the supplied RecordVersion is not the latest on the server side,
  15408. // this method will return FALSE and the caller should synchronize again via
  15409. // RecordVersionSynchronize() to avoid any missing update
  15410. // - if the supplied RecordVersion is the latest on the server side,
  15411. // this method will return TRUE and put the Callback notification in place
  15412. function RecordVersionSynchronizeSubscribeMaster(Table: TSQLRecordClass;
  15413. RecordVersion: TRecordVersion; const SlaveCallback: IServiceRecordVersionCallback): boolean; overload;
  15414. /// this method is called internally after any successfull deletion to
  15415. // ensure relational database coherency
  15416. // - reset all matching TRecordReference properties in the database Model,
  15417. // for database coherency, into 0
  15418. // - delete all records containing a matched TRecordReferenceToBeDeleted
  15419. // property value in the database Model (e.g. TSQLRecordHistory)
  15420. // - reset all matching TSQLRecord properties in the database Model,
  15421. // for database coherency, into 0
  15422. // - important notice: we don't use FOREIGN KEY constraints in this framework,
  15423. // and handle all integrity check within this method (it's therefore less
  15424. // error-prone, and more cross-database engine compatible)
  15425. function AfterDeleteForceCoherency(aTableIndex: integer; aID: TID): boolean; virtual;
  15426. /// update all BLOB fields of the supplied Value
  15427. // - this overridden method will execute the direct static class, if any
  15428. function UpdateBlobFields(Value: TSQLRecord): boolean; override;
  15429. /// get all BLOB fields of the supplied value from the remote server
  15430. // - this overridden method will execute the direct static class, if any
  15431. function RetrieveBlobFields(Value: TSQLRecord): boolean; override;
  15432. /// implement Server-Side TSQLRest unlocking
  15433. // - to be called e.g. after a Retrieve() with forupdate=TRUE
  15434. // - implements our custom UNLOCK REST-like verb
  15435. // - locking is handled by TSQLServer.Model
  15436. // - returns true on success
  15437. function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override;
  15438. /// end a transaction
  15439. // - implements REST END collection
  15440. // - write all pending TSQLVirtualTableJSON data to the disk
  15441. procedure Commit(SessionID: cardinal; RaiseException: boolean); override;
  15442. /// grant access to this database content from a dll using the global
  15443. // URIRequest() function
  15444. // - returns true if the URIRequest() function is set to this TSQLRestServer
  15445. // - returns false if a TSQLRestServer was already exported
  15446. // - client must release all memory acquired by URIRequest() with GlobalFree()
  15447. function ExportServer: boolean; overload;
  15448. {$ifdef MSWINDOWS}
  15449. /// declare the server on the local machine as a Named Pipe: allows
  15450. // TSQLRestClientURINamedPipe local or remote client connection
  15451. // - ServerApplicationName ('DBSERVER' e.g.) will be used to create a named
  15452. // pipe server identifier, it is of UnicodeString type since Delphi 2009
  15453. // (use of Unicode FileOpen() version)
  15454. // - this server identifier is appended to '\\.\pipe\mORMot_' to obtain
  15455. // the full pipe name to initiate ('\\.\pipe\mORMot_DBSERVER' e.g.)
  15456. // - this server identifier may also contain a fully qualified path
  15457. // ('\\.\pipe\ApplicationName' e.g.)
  15458. // - allows only one ExportServer*() by running process
  15459. // - returns true on success, false otherwise (ServerApplicationName already used?)
  15460. function ExportServerNamedPipe(const ServerApplicationName: TFileName): boolean;
  15461. /// end any currently initialized named pipe server
  15462. function CloseServerNamedPipe: boolean;
  15463. /// declare the server on the local machine to be accessible for local
  15464. // client connection, by using Windows messages
  15465. // - the data is sent and received by using the standard and fast WM_COPYDATA message
  15466. // - Windows messages are very fast (faster than named pipe and much faster
  15467. // than HTTP), but only work localy on the same computer
  15468. // - create a new Window Class with the supplied class name (UnicodeString
  15469. // since Delphi 2009 for direct use of Wide Win32 API), and instanciate
  15470. // a window which will handle pending WM_COPYDATA messages
  15471. // - the main server instance has to process the windows messages regularely
  15472. // (e.g. with Application.ProcessMessages)
  15473. // - ServerWindowName ('DBSERVER' e.g.) will be used to create a
  15474. // Window name identifier
  15475. // - allows only one ExportServer*() by running process
  15476. // - returns true on success, false otherwise (ServerWindowName already used?)
  15477. function ExportServerMessage(const ServerWindowName: string): boolean;
  15478. /// implement a message-based server response
  15479. // - this method is called automaticaly if ExportServerMessage() method
  15480. // was initilialized
  15481. // - you can also call this method from the WM_COPYDATA message handler
  15482. // of your main form, and use the TSQLRestClientURIMessage class to access
  15483. // the server instance from your clients
  15484. // - it will answer to the Client with another WM_COPYDATA message
  15485. // - message oriented architecture doesn't need any thread, but will use
  15486. // the main thread of your application
  15487. procedure AnswerToMessage(var Msg: TWMCopyData); message WM_COPYDATA;
  15488. /// end any currently initialized message-oriented server
  15489. function CloseServerMessage: boolean;
  15490. /// returns TRUE if remote connection is possible via named pipes or Windows
  15491. // messages
  15492. function ExportedAsMessageOrNamedPipe: Boolean;
  15493. {$endif}
  15494. /// Server initialization with a specified Database Model
  15495. // - if HandleUserAuthentication is false, will set URI access rights to
  15496. // 'Supervisor' (i.e. all R/W access) by default
  15497. // - if HandleUserAuthentication is true, will add TSQLAuthUser and
  15498. // TSQLAuthGroup to the TSQLModel (if not already there)
  15499. constructor Create(aModel: TSQLModel; aHandleUserAuthentication: boolean=false); reintroduce; virtual;
  15500. /// Server initialization with a temporary Database Model
  15501. // - a Model will be created with supplied tables, and owned by the server
  15502. // - if you instantiate a TSQLRestServerFullMemory or TSQLRestServerDB
  15503. // with this constructor, an in-memory engine will be created, with
  15504. // enough abilities to run regression tests, for instance
  15505. constructor CreateWithOwnModel(const Tables: array of TSQLRecordClass;
  15506. aHandleUserAuthentication: boolean=false; const aRoot: RawUTF8='root');
  15507. /// create a new minimal TSQLRestServer instance, to be used with
  15508. // external SQL or NoSQL storage
  15509. // - will try to instantiate an in-memory TSQLRestServerDB, and if
  15510. // mORMotSQLite3.pas is not linked, fallback to a TSQLRestServerFullMemory
  15511. // - used e.g. by TSQLRestMongoDBCreate() and TSQLRestExternalDBCreate()
  15512. class function CreateInMemoryForAllVirtualTables(aModel: TSQLModel;
  15513. aHandleUserAuthentication: boolean): TSQLRestServer;
  15514. /// release memory and any existing pipe initialized by ExportServer()
  15515. destructor Destroy; override;
  15516. /// you can call this method to prepare the server for shutting down
  15517. // - it will reject any incoming request from now on, and will wait until
  15518. // all pending requests are finished, for proper server termination
  15519. // - you could optionally save the current server state (e.g. user sessions)
  15520. // into a file, ready to be retrieved later on using SessionsLoadFromFile -
  15521. // note that this would work only for ORM sessions, NOT complex SOA state
  15522. // - this method is called by Destroy itself
  15523. procedure Shutdown(const aStateFileName: TFileName=''); virtual;
  15524. /// Missing tables are created if they don't exist yet for every TSQLRecord
  15525. // class of the Database Model
  15526. // - you must call explicitely this before having called StaticDataCreate()
  15527. // - all table description (even Unique feature) is retrieved from the Model
  15528. // - this method should also create additional fields, if the TSQLRecord definition
  15529. // has been modified; only field adding is mandatory, field renaming or
  15530. // field deleting are not allowed in the FrameWork (in such cases, you must
  15531. // create a new TSQLRecord type)
  15532. // - this virtual method do nothing by default - overridden versions should
  15533. // implement it as expected by the underlying storage engine (e.g. SQLite3
  15534. // or TSQLRestServerFullInMemory)
  15535. // - you can tune some options transmitted to the TSQLRecord.InitializeTable
  15536. // virtual methods, e.g. to avoid the automatic create of indexes
  15537. procedure CreateMissingTables(user_version: cardinal=0;
  15538. options: TSQLInitializeTableOptions=[]); virtual;
  15539. /// run the TSQLRecord.InitializeTable methods for all void tables of the model
  15540. // - can be used instead of CreateMissingTables e.g. for MongoDB storage
  15541. // - you can specify the creation options, e.g. INITIALIZETABLE_NOINDEX
  15542. procedure InitializeTables(Options: TSQLInitializeTableOptions);
  15543. /// create an external static in-memory database for a specific class
  15544. // - call it just after Create, before TSQLRestServerDB.CreateMissingTables;
  15545. // warning: if you don't call this method before CreateMissingTable method
  15546. // is called, the table will be created as a regular table by the main
  15547. // database engine, and won't be static
  15548. // - can load the table content from a file if a file name is specified
  15549. // (could be either JSON or compressed Binary format on disk)
  15550. // - you can define a particular external engine by setting a custom class -
  15551. // by default, it will create a TSQLRestStorageInMemory instance
  15552. // - this data handles basic REST commands, since no complete SQL interpreter
  15553. // can be implemented by TSQLRestStorage; to provide full SQL process,
  15554. // you should better use a Virtual Table class, inheriting e.g. from
  15555. // TSQLRecordVirtualTableAutoID associated with TSQLVirtualTableJSON/Binary
  15556. // via a Model.VirtualTableRegister() call before TSQLRestServer.Create
  15557. // - return nil on any error, or an EModelException if the class is not in
  15558. // the database model
  15559. function StaticDataCreate(aClass: TSQLRecordClass;
  15560. const aFileName: TFileName = ''; aBinaryFile: boolean=false;
  15561. aServerClass: TSQLRestStorageInMemoryClass=nil): TSQLRestStorage;
  15562. /// register an external static storage for a given table
  15563. // - will be added to StaticDataServer[] internal list
  15564. // - called e.g. by StaticDataCreate(), RemoteDataCreate() or
  15565. // StaticMongoDBRegister()
  15566. function StaticDataAdd(aStaticData: TSQLRestStorage): boolean;
  15567. /// create an external static redirection for a specific class
  15568. // - call it just after Create, before TSQLRestServerDB.CreateMissingTables;
  15569. // warning: if you don't call this method before CreateMissingTable method
  15570. // is called, the table will be created as a regular table by the main
  15571. // database engine, and won't be static
  15572. // - the specified TSQLRecord will have all its CRUD / ORM methods be
  15573. // redirected to aRemoteRest, which may be a TSQLRestClient or another
  15574. // TSQLRestServer instance (e.g. a fast SQLITE_MEMORY_DATABASE_NAME)
  15575. // - if aRemoteRest is a TSQLRestClient, it should have been authenticated
  15576. // to the remote TSQLRestServer, so that CRUD / ORM operations would pass
  15577. // - this would enable easy creation of proxies, or local servers, with they
  15578. // own cache and data model - e.g. a branch office server which may server
  15579. // its local client over Ethernet, but communicating to a main mORMot
  15580. // server via Internet, storing the corporate data in the main office server
  15581. function RemoteDataCreate(aClass: TSQLRecordClass; aRemoteRest: TSQLRest): TSQLRestStorageRemote; virtual;
  15582. /// call this method when the internal DB content is known to be invalid
  15583. // - by default, all REST/CRUD requests and direct SQL statements are
  15584. // scanned and identified as potentially able to change the internal SQL/JSON
  15585. // cache used at SQLite3 database level; but some virtual tables (e.g.
  15586. // TSQLRestStorageExternal classes defined in mORMotDB) could flush
  15587. // the database content without proper notification
  15588. // - this default implementation will just do nothing, but mORMotSQlite3
  15589. // unit will call TSQLDataBase.CacheFlush method
  15590. procedure FlushInternalDBCache; virtual;
  15591. /// you can call this method in TThread.Execute to ensure that
  15592. // the thread will be taken in account during process
  15593. // - caller must specify the TThread instance running
  15594. // - used e.g. for optExecInMainThread option in TServiceMethodExecute
  15595. // - this default implementation will call the methods of all its internal
  15596. // TSQLRestStorage instances
  15597. // - this method shall be called from the thread just initiated: e.g.
  15598. // if you call it from the main thread, it may fail to prepare resources
  15599. procedure BeginCurrentThread(Sender: TThread); override;
  15600. /// you can call this method just before a thread is finished to ensure
  15601. // e.g. that the associated external DB connection will be released
  15602. // - this default implementation will call the methods of all its internal
  15603. // TSQLRestStorage instances, allowing e.g. TSQLRestStorageExternal
  15604. // instances to clean their thread-specific connections
  15605. // - this method shall be called from the thread about to be terminated: e.g.
  15606. // if you call it from the main thread, it may fail to release resources
  15607. // - it is set e.g. by TSQLite3HttpServer to be called from HTTP threads,
  15608. // or by TSQLRestServerNamedPipeResponse for named-pipe server cleaning
  15609. procedure EndCurrentThread(Sender: TThread); override;
  15610. /// implement a generic local, piped or HTTP/1.1 provider
  15611. // - this is the main entry point of the server, from the client side
  15612. // - default implementation calls protected methods EngineList() Retrieve()
  15613. // Add() Update() Delete() UnLock() EngineExecute() above, which must be overridden by
  15614. // the TSQLRestServer child
  15615. // - for 'GET ModelRoot/TableName', url parameters can be either "select" and
  15616. // "where" (to specify a SQL Query, from the SQLFromSelectWhere function),
  15617. // either "sort", "dir", "startIndex", "results", as expected by the YUI
  15618. // DataSource Request Syntax for data pagination - see
  15619. // http://developer.yahoo.com/yui/datatable/#data
  15620. // - execution of this method could be monitored via OnBeforeURI and OnAfterURI
  15621. // event handlers
  15622. procedure URI(var Call: TSQLRestURIParams); virtual;
  15623. /// create an index for the specific FieldName
  15624. // - will call CreateSQLMultiIndex() internaly
  15625. function CreateSQLIndex(Table: TSQLRecordClass; const FieldName: RawUTF8;
  15626. Unique: boolean; const IndexName: RawUTF8=''): boolean; overload;
  15627. /// create one or multiple index(es) for the specific FieldName(s)
  15628. function CreateSQLIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8;
  15629. Unique: boolean): boolean; overload;
  15630. /// create one index for all specific FieldNames at once
  15631. // - will call any static engine for the index creation of such tables, or
  15632. // execute a CREATE INDEX IF NOT EXISTS on the main engine
  15633. // - note that with SQLite3, your database schema should never contain two
  15634. // indices where one index is a prefix of the other, e.g. if you defined:
  15635. // ! aServer.CreateSQLMultiIndex(TEmails, ['Email','GroupID'], True);
  15636. // Then the following index is not mandatory for SQLite3:
  15637. // ! aServer.CreateSQLIndex(TEmails, 'Email', False);
  15638. // see "1.6 Multi-Column Indices" in @http://www.sqlite.org/queryplanner.html
  15639. function CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8;
  15640. Unique: boolean; IndexName: RawUTF8=''): boolean; virtual;
  15641. /// call this method to add an authentication method to the server
  15642. // - will return the just created TSQLRestServerAuthentication instance,
  15643. // or the existing instance if it has already been registered
  15644. // - you can use this method to tune the authencation, e.g. if you have
  15645. // troubles with AJAX asynchronous callbacks:
  15646. // ! (aServer.AuthenticationRegister(TSQLRestServerAuthenticationDefault) as
  15647. // ! TSQLRestServerAuthenticationSignedURI).NoTimeStampCoherencyCheck := true;
  15648. function AuthenticationRegister(
  15649. aMethod: TSQLRestServerAuthenticationClass): TSQLRestServerAuthentication; overload;
  15650. /// call this method to add several authentication methods to the server
  15651. // - if TSQLRestServer.Create() constructor is called with aHandleUserAuthentication
  15652. // set to TRUE, it will register the two following classes:
  15653. // ! AuthenticationRegister([TSQLRestServerAuthenticationDefault,TSQLRestServerAuthenticationSSPI]);
  15654. procedure AuthenticationRegister(const aMethods: array of TSQLRestServerAuthenticationClass); overload;
  15655. /// call this method to remove an authentication method to the server
  15656. procedure AuthenticationUnregister(aMethod: TSQLRestServerAuthenticationClass); overload;
  15657. /// call this method to remove several authentication methods to the server
  15658. procedure AuthenticationUnregister(const aMethods: array of TSQLRestServerAuthenticationClass); overload;
  15659. /// call this method to remove all authentication methods to the server
  15660. procedure AuthenticationUnregisterAll;
  15661. /// add all published methods of a given object instance to the method-based
  15662. // list of services
  15663. // - all those published method signature should match TSQLRestServerCallBack
  15664. procedure ServiceMethodRegisterPublishedMethods(const aPrefix: RawUTF8;
  15665. aInstance: TObject);
  15666. /// direct registration of a method for a given low-level event handler
  15667. procedure ServiceMethodRegister(aMethodName: RawUTF8;
  15668. const aEvent: TSQLRestServerCallBack; aByPassAuthentication: boolean=false);
  15669. /// call this method to disable Authentication method check for a given
  15670. // published method-based service name
  15671. // - by default, only Auth and TimeStamp methods do not require the RESTful
  15672. // authentication of the URI; you may call this method to add another method
  15673. // to the list (e.g. for returning some HTML content from a public URI)
  15674. // - if the supplied aMethodName='', all method-based services would
  15675. // bypass the authenticaton process
  15676. procedure ServiceMethodByPassAuthentication(const aMethodName: RawUTF8);
  15677. /// retrieve detailed statistics about a method-based service use
  15678. // - will return a reference to the actual alive item: caller should
  15679. // not free the returned instance
  15680. property ServiceMethodStat[const aMethod: RawUTF8]: TSynMonitorInputOutput
  15681. read GetServiceMethodStat;
  15682. /// used e.g. by IAdministratedDaemon to implement "pseudo-SQL" commands
  15683. procedure AdministrationExecute(const DatabaseName,SQL: RawUTF8;
  15684. var result: TServiceCustomAnswer); override;
  15685. /// compute a JSON description of all available services, and its public URI
  15686. // - the JSON object matches the TServicesPublishedInterfaces record type
  15687. // - used by TSQLRestClientURI.ServicePublishOwnInterfaces to register all
  15688. // the services supportes by the client itself
  15689. // - warning: the public URI should have been set via SetPublicURI()
  15690. function ServicesPublishedInterfaces: RawUTF8;
  15691. /// the HTTP server should call this method so that ServicesPublishedInterfaces
  15692. // registration would be able to work
  15693. procedure SetPublicURI(const Address,Port: RawUTF8);
  15694. /// a list of the services associated by all clients of this server instance
  15695. // - when a client connects to this server, it would publish its own services
  15696. // (when checking its interface contract), so that they may be identified
  15697. property AssociatedServices: TServicesPublishedInterfacesList read fAssociatedServices;
  15698. /// returns a copy of the user associated to a session ID
  15699. // - returns nil if the session does not exist (e.g. if authentication is
  15700. // disabled)
  15701. // - caller MUST release the TSQLAuthUser instance returned (if not nil)
  15702. // - this method IS thread-safe, and call internaly fSessions.Lock
  15703. // (the returned TSQLAuthUser is a private copy from fSessions[].User instance,
  15704. // in order to be really thread-safe)
  15705. // - the returned TSQLAuthUser instance will have GroupRights=nil but will
  15706. // have ID, LogonName, DisplayName, PasswordHashHexa and Data fields available
  15707. function SessionGetUser(aSessionID: Cardinal): TSQLAuthUser;
  15708. /// persist all in-memory sessions into a compressed binary file
  15709. // - you should not call this method it directly, but rather use Shutdown()
  15710. // with a StateFileName parameter - to be used e.g. for a short maintainance
  15711. // server shutdown, without loosing the current logged user sessions
  15712. // - this method IS thread-safe, and call internaly fSessions.Lock
  15713. procedure SessionsSaveToFile(const aFileName: TFileName);
  15714. /// re-create all in-memory sessions from a compressed binary file
  15715. // - typical use is after a server restart, with the file supplied to the
  15716. // Shutdown() method: it could be used e.g. for a short maintainance server
  15717. // shutdown, without loosing the current logged user sessions
  15718. // - WARNING: this method would restore authentication sessions for the ORM,
  15719. // but not any complex state information used by interface-based services,
  15720. // like sicClientDriven class instances - DO NOT use this feature with SOA
  15721. // - this method IS thread-safe, and call internaly fSessions.Lock
  15722. procedure SessionsLoadFromFile(const aFileName: TFileName;
  15723. andDeleteExistingFileAfterRead: boolean);
  15724. /// retrieve all current session information as a JSON array
  15725. function SessionsAsJson: RawJSON;
  15726. /// register a Service class on the server side
  15727. // - this methods expects a class to be supplied, and the exact list of
  15728. // interfaces to be registered to the server (e.g. [TypeInfo(IMyInterface)])
  15729. // and implemented by this class
  15730. // - class can be any TInterfacedObject, but TInterfacedObjectWithCustomCreate
  15731. // can be used if you need an overridden constructor
  15732. // - instance implementation pattern will be set by the appropriate parameter
  15733. // - will return the first of the registered TServiceFactoryServer created
  15734. // on success (i.e. the one corresponding to the first item of the aInterfaces
  15735. // array), or nil if registration failed (e.g. if any of the supplied interfaces
  15736. // is not implemented by the given class)
  15737. // - you can use the returned TServiceFactoryServer instance to set the
  15738. // expected security parameters associated with this interface
  15739. // - the same implementation class can be used to handle several interfaces
  15740. // (just as Delphi allows to do natively)
  15741. function ServiceRegister(aImplementationClass: TInterfacedClass;
  15742. const aInterfaces: array of PTypeInfo;
  15743. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  15744. const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload; virtual;
  15745. /// register a Service instance on the server side
  15746. // - this methods expects a class instance to be supplied, and the exact list
  15747. // of interfaces to be registered to the server (e.g. [TypeInfo(IMyInterface)])
  15748. // and implemented by this shared instance
  15749. // - as a consequence, instance implementation pattern will always be sicShared
  15750. // - will return the first of the registered TServiceFactoryServer created
  15751. // on success (i.e. the one corresponding to the first item of the aInterfaces
  15752. // array), or nil if registration failed (e.g. if any of the supplied interfaces
  15753. // is not implemented by the given class)
  15754. // - you can use the returned TServiceFactoryServer instance to set the
  15755. // expected security parameters associated with this interface
  15756. // - the same implementation class can be used to handle several interfaces
  15757. // (just as Delphi allows to do natively)
  15758. function ServiceRegister(aSharedImplementation: TInterfacedObject;
  15759. const aInterfaces: array of PTypeInfo; const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload; virtual;
  15760. /// register a remote Service via its interface
  15761. // - this overloaded method will register a remote Service, accessed via the
  15762. // supplied TSQLRest(ClientURI) instance: it can be available in the main
  15763. // TSQLRestServer.Services property, but execution will take place on a
  15764. // remote server - may be used e.g. for dedicated hosting of services (in
  15765. // a DMZ for instance)
  15766. // - this methods expects a list of interfaces to be registered to the client
  15767. // (e.g. [TypeInfo(IMyInterface)])
  15768. // - instance implementation pattern will be set by the appropriate parameter
  15769. // - will return true on success, false if registration failed (e.g. if any of
  15770. // the supplied interfaces is not correct or is not available on the server)
  15771. // - that is, server side will be called to check for the availability of
  15772. // each interface
  15773. // - you can specify an optional custom contract for the first interface
  15774. function ServiceRegister(aClient: TSQLRest; const aInterfaces: array of PTypeInfo;
  15775. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  15776. const aContractExpected: RawUTF8=''): boolean; overload; virtual;
  15777. /// register a Service class on the server side
  15778. // - this method expects the interface(s) to have been registered previously:
  15779. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]);
  15780. function ServiceDefine(aImplementationClass: TInterfacedClass;
  15781. const aInterfaces: array of TGUID;
  15782. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  15783. const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload;
  15784. /// register a Service instance on the server side
  15785. // - this method expects the interface(s) to have been registered previously:
  15786. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]);
  15787. // - the supplied aSharedImplementation will be owned by this Server instance
  15788. function ServiceDefine(aSharedImplementation: TInterfacedObject;
  15789. const aInterfaces: array of TGUID; const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload;
  15790. /// register a remote Service via its interface
  15791. // - this method expects the interface(s) to have been registered previously:
  15792. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]);
  15793. function ServiceDefine(aClient: TSQLRest; const aInterfaces: array of TGUID;
  15794. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  15795. const aContractExpected: RawUTF8=''): boolean; overload;
  15796. /// access or initialize the internal IoC resolver, used for interface-based
  15797. // remote services, and more generaly any Services.Resolve() call
  15798. // - create and initialize the internal TServiceContainerServer if no
  15799. // service interface has been registered yet
  15800. // - may be used to inject some dependencies, which are not interface-based
  15801. // remote services, but internal IoC, without the ServiceRegister()
  15802. // or ServiceDefine() methods - e.g.
  15803. // ! aRest.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(aRest)],true);
  15804. // - this overriden method would return a TServiceContainerServer instance
  15805. // - you may enable SOA audit trail for all methods execution:
  15806. // ! (aRestSOAServer.ServiceContainer as TServiceContainerServer).SetServiceLog(
  15807. // ! aRestLogServer,TSQLRecordServiceLog);
  15808. function ServiceContainer: TServiceContainer; override;
  15809. /// compute the full statistics about this server, as JSON
  15810. // - is a wrapper around the Stats() method-based service, setting withall=1
  15811. function FullStatsAsJson: RawUTF8; virtual;
  15812. /// compute the full statistics about this server, as a TDocVariant document
  15813. // - is a wrapper around the Stats() method-based service, setting withall=1
  15814. function FullStatsAsDocVariant: variant;
  15815. /// read-only access to the list of registered server-side authentication
  15816. // methods, used for session creation
  15817. // - note that the exact number or registered services in this list is
  15818. // stored in the AuthenticationSchemesCount property
  15819. property AuthenticationSchemes: TSQLRestServerAuthenticationDynArray
  15820. read fSessionAuthentication;
  15821. /// how many authentication methods are registered in AuthenticationSchemes
  15822. property AuthenticationSchemesCount: integer
  15823. read GetAuthenticationSchemesCount;
  15824. /// retrieve the TSQLRestStorage instance used to store and manage
  15825. // a specified TSQLRecordClass in memory
  15826. // - has been associated by the StaticDataCreate method
  15827. property StaticDataServer[aClass: TSQLRecordClass]: TSQLRest
  15828. read GetStaticDataServer;
  15829. /// retrieve a running TSQLRestStorage virtual table
  15830. // - associated e.g. to a 'JSON' or 'Binary' virtual table module, or may
  15831. // return a TSQLRestStorageExternal instance (as defined in mORMotDB)
  15832. // - this property will return nil if there is no Virtual Table associated
  15833. // or if the corresponding module is not a TSQLVirtualTable
  15834. // (e.g. "pure" static tables registered by StaticDataCreate would be
  15835. // accessible only via StaticDataServer[], not via StaticVirtualTable[])
  15836. // - has been associated by the TSQLModel.VirtualTableRegister method or
  15837. // the VirtualTableExternalRegister() global function
  15838. property StaticVirtualTable[aClass: TSQLRecordClass]: TSQLRest
  15839. read GetVirtualTable;
  15840. /// the options specified to TSQLRestServer.CreateMissingTables
  15841. // - as expected by TSQLRecord.InitializeTable methods
  15842. property CreateMissingTablesOptions: TSQLInitializeTableOptions
  15843. read fCreateMissingTablesOptions;
  15844. /// the URI to redirect any plain GET on root URI, without any method
  15845. // - could be used to ease access from web browsers URI
  15846. property RootRedirectGet: RawUTF8 read fRootRedirectGet write fRootRedirectGet;
  15847. /// you can force this property to TRUE so that any Delete() would not
  15848. // write to the TSQLRecordTableDelete table for TRecordVersion tables
  15849. // - to be used when applying a TSQLRestBatch instance as returned by
  15850. // RecordVersionSynchronizeToBatch()
  15851. property RecordVersionDeleteIgnore: boolean
  15852. read fRecordVersionDeleteIgnore write fRecordVersionDeleteIgnore;
  15853. published
  15854. /// set this property to true to transmit the JSON data in a "not expanded" format
  15855. // - not directly compatible with Javascript object list decode: not to be
  15856. // used in AJAX environnement (like in TSQLite3HttpServer)
  15857. // - but transmitted JSON data is much smaller if set it's set to FALSE, and
  15858. // if you use a Delphi Client, parsing will be also faster and memory usage
  15859. // will be lower
  15860. // - By default, the NoAJAXJSON property is set to TRUE in
  15861. // TSQLRestServer.ExportServerNamedPipe: if you use named pipes for communication,
  15862. // you probably won't use javascript because browser communicates via HTTP!
  15863. // - But otherwise, NoAJAXJSON property is set to FALSE. You could force its
  15864. // value to TRUE and you'd save some bandwidth if you don't use javascript:
  15865. // even the parsing of the JSON Content will be faster with Delphi client
  15866. // if JSON content is not expanded
  15867. // - the "expanded" or standard/AJAX layout allows you to create pure JavaScript
  15868. // objects from the JSON content, because the field name / JavaScript object
  15869. // property name is supplied for every value
  15870. // - the "not expanded" layout, NoAJAXJSON property is set to TRUE,
  15871. // reflects exactly the layout of the SQL request - first line contains the
  15872. // field names, then all next lines are the field content
  15873. // - is in fact stored in rsoNoAJAXJSON item in Options property
  15874. property NoAJAXJSON: boolean read GetNoAJAXJSON write SetNoAJAXJSON;
  15875. /// allow to customize how TSQLRestServer.URI process the requests
  15876. // - e.g. if HTML_SUCCESS with no body should be translated into HTML_NOCONTENT
  15877. property Options: TSQLRestServerOptions read fOptions write fOptions;
  15878. /// set to true if the server will handle per-user authentication and
  15879. // access right management
  15880. // - i.e. if the associated TSQLModel contains TSQLAuthUser and
  15881. // TSQLAuthGroup tables (set by constructor)
  15882. property HandleAuthentication: boolean read fHandleAuthentication;
  15883. /// allow to by-pass Authentication for a given set of HTTP verbs
  15884. // - by default, RESTful access to the ORM would follow HandleAuthentication
  15885. /// setting: but you could define some HTTP verb to this property, which
  15886. // would by-pass the authentication - may be used e.g. for public GET
  15887. // of the content by an AJAX client
  15888. property BypassORMAuthentication: TSQLURIMethods read fBypassORMAuthentication write fBypassORMAuthentication;
  15889. /// read-only access to the high-level Server statistics
  15890. // - see ServiceMethodStat[] for information about method-based services,
  15891. // or TServiceFactoryServer.Stats / Stat[] for interface-based services
  15892. // - statistics are available remotely as JSON from the Stat() method
  15893. property Stats: TSQLRestServerMonitor read fStats;
  15894. /// which level of detailed information is gathered
  15895. // - by default, contains SERVERDEFAULTMONITORLEVELS, i.e.
  15896. // ! [mlTables,mlMethods,mlInterfaces,mlSQLite3]
  15897. // - you can add mlSessions to maintain per-session statistics: this would
  15898. // lead into a slightly higher memory consumption, for each session
  15899. property StatLevels: TSQLRestServerMonitorLevels read fStatLevels write fStatLevels;
  15900. /// could be set to track statistic from Stats information
  15901. // - it may be e.g. a TSynMonitorUsageRest instance for REST storage
  15902. property StatUsage: TSynMonitorUsage read fStatUsage write SetStatUsage;
  15903. /// this property can be left to its TRUE default value, to handle any
  15904. // TSQLVirtualTableJSON static tables (module JSON or BINARY) with direct
  15905. // calls to the storage instance
  15906. // - is set to TRUE by default to enable faster Direct mode
  15907. // - in Direct mode, GET/POST/PUT/DELETE of individual records (or BLOB fields)
  15908. // from URI() will call directly the corresponding TSQLRestStorage
  15909. // instance, for better speed for most used RESTful operations; but complex
  15910. // SQL requests (e.g. joined SELECT) will rely on the main SQL engine
  15911. // - if set to false, will use the main SQLite3 engine for all statements
  15912. // (should not to be used normaly, because it will add unnecessary overhead)
  15913. property StaticVirtualTableDirect: boolean read fVirtualTableDirect
  15914. write fVirtualTableDirect;
  15915. /// the class inheriting from TSQLAuthUser, as defined in the model
  15916. // - during authentication, this class will be used for every TSQLAuthUser
  15917. // table access
  15918. // - see also the OnAuthenticationUserRetrieve optional event handler
  15919. property SQLAuthUserClass: TSQLAuthUserClass read fSQLAuthUserClass;
  15920. /// the class inheriting from TSQLAuthGroup, as defined in the model
  15921. // - during authentication, this class will be used for every TSQLAuthGroup
  15922. // table access
  15923. property SQLAuthGroupClass: TSQLAuthGroupClass read fSQLAuthGroupClass;
  15924. /// the class inheriting from TSQLRecordTableDeleted, as defined in the model
  15925. // - during authentication, this class will be used for storing a trace of
  15926. // every deletion of table rows containing a TRecordVersion published field
  15927. property SQLRecordVersionDeleteTable: TSQLRecordTableDeletedClass
  15928. read fSQLRecordVersionDeleteTable;
  15929. /// the class inheriting from TAuthSession to handle in-memory sessions
  15930. // - since all sessions data remain in memory, ensure they are not taking
  15931. // too much resource (memory or process time)
  15932. property SessionClass: TAuthSessionClass read fSessionClass write fSessionClass;
  15933. published { standard method-based services }
  15934. /// REST service accessible from ModelRoot/Stat URI to gather detailed information
  15935. // - returns the current execution statistics of this server, as a JSON object
  15936. // - this method would require an authenticated client, for safety
  15937. // - by default, will return the high-level information of this server
  15938. // - will return human-readable JSON layout if ModelRoot/Stat/json is used, or
  15939. // the corresponding XML content if ModelRoot/Stat/xml is used
  15940. // - you can define withtables, withmethods, withinterfaces, withsessions or
  15941. // withsqlite3 additional parameters to return detailed information about
  15942. // method-based services, interface-based services, per session statistics,
  15943. // or prepared SQLite3 SQL statement timing (for a TSQLRestServerDB instance)
  15944. // ! Client.CallBackGet('stat',['withtables',true,'withmethods',true,
  15945. // ! 'withinterfaces',true,'withsessions',true,'withsqlite3',true],stats);
  15946. // - defining a 'withall' parameter will retrieve all available statistics
  15947. // - note that TSQLRestServer.StatLevels property will enable statistics
  15948. // gathering for tables, methods, interfaces, sqlite3 or sessions
  15949. // - a specific findservice=ServiceName parameter would not return any
  15950. // statistics, but matching URIs from the server AssociatedServices list
  15951. procedure Stat(Ctxt: TSQLRestServerURIContext);
  15952. /// REST service accessible from ModelRoot/Auth URI
  15953. // - called by the clients for authentication and session management
  15954. // - this method would require an authenticated client, by design
  15955. // - this global callback method is thread-safe
  15956. procedure Auth(Ctxt: TSQLRestServerURIContext);
  15957. /// REST service accessible from the ModelRoot/TimeStamp URI
  15958. // - returns the server time stamp TTimeLog/Int64 value as UTF-8 text
  15959. // - this method would not require an authenticated client
  15960. // - hidden ModelRoot/TimeStamp/info command would return basic execution
  15961. // information, less verbose (and sensitive) than Stat(), calling virtual
  15962. // InternalInfo() protected method
  15963. procedure TimeStamp(Ctxt: TSQLRestServerURIContext);
  15964. /// REST service accessible from the ModelRoot/CacheFlush URI
  15965. // - it will flush the server result cache
  15966. // - this method shall be called by the clients when the Server cache may be
  15967. // not consistent any more (e.g. after a direct write to an external database)
  15968. // - this method would require an authenticated client, for safety
  15969. // - GET ModelRoot/CacheFlush URI will flush the whole Server cache,
  15970. // for all tables
  15971. // - GET ModelRoot/CacheFlush/TableName URI will flush the specified
  15972. // table cache
  15973. // - GET ModelRoot/CacheFlush/TableName/TableID URI will flush the content
  15974. // of the specified record
  15975. // - in addition, POST ModelRoot/CacheFlush/_callback_ URI will be called
  15976. // automatically by the client, to notify the server that an interface
  15977. // callback instance has been released
  15978. procedure CacheFlush(Ctxt: TSQLRestServerURIContext);
  15979. /// REST service accessible from the ModelRoot/Batch URI
  15980. // - will execute a set of RESTful commands, in a single step, with optional
  15981. // automatic SQL transaction generation
  15982. // - this method would require an authenticated client, for safety
  15983. // - expect input as JSON commands:
  15984. // & '{"Table":["cmd":values,...]}'
  15985. // or for multiple tables:
  15986. // & '["cmd@Table":values,...]'
  15987. // with cmd in POST/PUT with {object} as value or DELETE with ID
  15988. // - returns an array of integers: '[200,200,...]' or '["OK"]' if all
  15989. // returned status codes are 200 (HTML_SUCCESS)
  15990. // - URI are either 'ModelRoot/TableName/Batch' or 'ModelRoot/Batch'
  15991. procedure Batch(Ctxt: TSQLRestServerURIContext);
  15992. end;
  15993. /// REST class with direct access to an external database engine
  15994. // - you can set an alternate per-table database engine by using this class
  15995. // - this abstract class is to be overridden with a proper implementation
  15996. // (e.g. TSQLRestStorageInMemory in this unit, or TSQLRestStorageExternal
  15997. // from mORMotDB unit, or TSQLRestStorageMongoDB from mORMotMongoDB unit)
  15998. TSQLRestStorage = class(TSQLRest)
  15999. protected
  16000. fStoredClass: TSQLRecordClass;
  16001. fStoredClassProps: TSQLModelRecordProperties;
  16002. fStoredClassRecordProps: TSQLRecordProperties;
  16003. fStorageLockShouldIncreaseOwnerInternalState: boolean;
  16004. fStorageLockLogTrace: boolean;
  16005. fModified: boolean;
  16006. fOwner: TSQLRestServer;
  16007. fStorageCriticalSection: TRTLCriticalSection;
  16008. fStorageCriticalSectionCount: integer;
  16009. fBasicSQLCount: RawUTF8;
  16010. fBasicSQLHasRows: array[boolean] of RawUTF8;
  16011. /// any set bit in this field indicates UNIQUE field value
  16012. fIsUnique: TSQLFieldBits;
  16013. /// allow to force refresh for a given Static table
  16014. // - default FALSE means to return the main TSQLRestServer.InternalState
  16015. // - TRUE indicates that OutInternalState := cardinal(-1) will be returned
  16016. fOutInternalStateForcedRefresh: boolean;
  16017. procedure RecordVersionFieldHandle(Occasion: TSQLOccasion;
  16018. var Decoder: TJSONObjectDecoder);
  16019. /// override this method if you want to update the refresh state
  16020. // - returns FALSE if the static table content was not modified (default
  16021. // method implementation is to always return FALSE)
  16022. // - returns TRUE if the table has been refreshed and its content was modified:
  16023. // therefore the client will know he'll need to refresh some content
  16024. function RefreshedAndModified: boolean; virtual;
  16025. /// TSQLRestServer.URI use it for Static.EngineList to by-pass virtual table
  16026. // - this default implementation will return TRUE and replace SQL with
  16027. // SQLSelectAll[true] if it SQL equals SQLSelectAll[false] (i.e. 'SELECT *')
  16028. // - this method is called only if the WHERE clause of SQL refers to the
  16029. // static table name only (not needed to check it twice)
  16030. function AdaptSQLForEngineList(var SQL: RawUTF8): boolean; virtual;
  16031. function GetStoredClassName: RawUTF8;
  16032. function GetCurrentSessionUserID: TID; override;
  16033. public
  16034. /// initialize the abstract storage data
  16035. constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer); reintroduce; virtual;
  16036. /// finalize the storage instance
  16037. destructor Destroy; override;
  16038. /// should be called before any access to the storage content
  16039. // - and protected with a try ... finally StorageUnLock; end section
  16040. procedure StorageLock(WillModifyContent: boolean); virtual;
  16041. /// should be called after any StorageLock-protected access to the content
  16042. // - e.g. protected with a try ... finally StorageUnLock; end section
  16043. procedure StorageUnLock; virtual;
  16044. /// you can call this method in TThread.Execute to ensure that
  16045. // the thread will be taken in account during process
  16046. // - this overridden method will do nothing (should have been already made
  16047. // at TSQLRestServer caller level)
  16048. // - children classes may inherit from this method to notify e.g.
  16049. // a third party process (like proper OLE initialization)
  16050. procedure BeginCurrentThread(Sender: TThread); override;
  16051. /// you can call this method just before a thread is finished to ensure
  16052. // e.g. that the associated external DB connection will be released
  16053. // - this overridden method will do nothing (should have been already made
  16054. // at TSQLRestServer caller level)
  16055. // - children classes may inherit from this method to notify e.g.
  16056. // a third party process (like proper OLE initialization)
  16057. procedure EndCurrentThread(Sender: TThread); override;
  16058. /// implement TSQLRest unlocking (UNLOCK verb)
  16059. // - to be called e.g. after a Retrieve() with forupdate=TRUE
  16060. // - locking is handled at (Owner.)Model level
  16061. // - returns true on success
  16062. function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override;
  16063. /// overridden method calling the owner (if any) to guess if this record
  16064. // can be updated or deleted
  16065. function RecordCanBeUpdated(Table: TSQLRecordClass; ID: TID; Action: TSQLEvent;
  16066. ErrorMsg: PRawUTF8 = nil): boolean; override;
  16067. /// create one index for all specific FieldNames at once
  16068. // - do nothing method: will return FALSE (aka error)
  16069. function CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8;
  16070. Unique: boolean; IndexName: RawUTF8=''): boolean; virtual;
  16071. /// search for a numerical field value
  16072. // - return true on success (i.e. if some values have been added to ResultID)
  16073. // - store the results into the ResultID dynamic array
  16074. // - faster than OneFieldValues method, which creates a temporary JSON content
  16075. // - this default implementation will call the overloaded SearchField()
  16076. // value after conversion of the FieldValue into RawUTF8
  16077. function SearchField(const FieldName: RawUTF8; FieldValue: Int64;
  16078. out ResultID: TIDDynArray): boolean; overload; virtual;
  16079. /// search for a field value, according to its SQL content representation
  16080. // - return true on success (i.e. if some values have been added to ResultID)
  16081. // - store the results into the ResultID dynamic array
  16082. // - faster than OneFieldValues method, which creates a temporary JSON content
  16083. function SearchField(const FieldName, FieldValue: RawUTF8;
  16084. out ResultID: TIDDynArray): boolean; overload; virtual; abstract;
  16085. /// access or initialize the internal IoC resolver
  16086. // - this overriden method would return always nil, since IoC only makes
  16087. // sense at TSQLRestClient and TSQLRestServer level
  16088. function ServiceContainer: TServiceContainer; override;
  16089. /// read only access to a boolean value set to true if table data was modified
  16090. property Modified: boolean read fModified write fModified;
  16091. /// read only access to the ORM properties of the associated record type
  16092. // - may be nil if this instance is not associated with a TSQLModel
  16093. property StoredClassProps: TSQLModelRecordProperties read fStoredClassProps;
  16094. /// read only access to the RTTI properties of the associated record type
  16095. property StoredClassRecordProps: TSQLRecordProperties read fStoredClassRecordProps;
  16096. /// read only access to the TSQLRestServer using this storage engine
  16097. property Owner: TSQLRestServer read fOwner;
  16098. /// enable low-level trace of StorageLock/StorageUnlock methods
  16099. // - may be used to resolve low-level race conditions
  16100. property StorageLockLogTrace: boolean read fStorageLockLogTrace write fStorageLockLogTrace;
  16101. /// read only access to the class defining the record type stored in this
  16102. // REST storage
  16103. property StoredClass: TSQLRecordClass read fStoredClass;
  16104. published
  16105. /// name of the class defining the record type stored in this REST storage
  16106. property StoredClassName: RawUTF8 read GetStoredClassName;
  16107. end;
  16108. /// event prototype called by TSQLRestStorageInMemory.FindWhereEqual() or
  16109. // TSQLRestStorageInMemory.ForEach() methods
  16110. // - aDest is an opaque pointer, as supplied to FindWhereEqual(), which may
  16111. // point e.g. to a result list, or a shared variable to apply the process
  16112. // - aRec will point to the corresponding item
  16113. // - aIndex will identify the item index in the internal list
  16114. TFindWhereEqualEvent = procedure(aDest: pointer; aRec: TSQLRecord; aIndex: integer) of object;
  16115. /// abstract REST storage exposing some internal TSQLRecord-based methods
  16116. TSQLRestStorageRecordBased = class(TSQLRestStorage)
  16117. protected
  16118. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  16119. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  16120. public
  16121. /// manual Add of a TSQLRecord
  16122. // - returns the ID created on success
  16123. // - returns -1 on failure (not UNIQUE field value e.g.)
  16124. // - on success, the Rec instance is added to the Values[] list: caller
  16125. // doesn't need to Free it
  16126. function AddOne(Rec: TSQLRecord; ForceID: boolean; const SentData: RawUTF8): TID; virtual; abstract;
  16127. /// manual Retrieval of a TSQLRecord field values
  16128. // - an instance of the associated static class is created
  16129. // - and all its properties are filled from the Items[] values
  16130. // - caller can modify these properties, then use UpdateOne() if the changes
  16131. // have to be stored inside the Items[] list
  16132. // - calller must always free the returned instance
  16133. // - returns NIL if any error occured, e.g. if the supplied aID was incorrect
  16134. // - method available since a TSQLRestStorage instance may be created
  16135. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16136. function GetOne(aID: TID): TSQLRecord; virtual; abstract;
  16137. /// manual Update of a TSQLRecord field values
  16138. // - Rec.ID specifies which record is to be updated
  16139. // - will update all properties, including BLOB fields and such
  16140. // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID)
  16141. // - method available since a TSQLRestStorage instance may be created
  16142. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16143. function UpdateOne(Rec: TSQLRecord; const SentData: RawUTF8): boolean; overload; virtual; abstract;
  16144. /// manual Update of a TSQLRecord field values from an array of TSQLVar
  16145. // - will update all properties, including BLOB fields and such
  16146. // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID)
  16147. // - method available since a TSQLRestStorage instance may be created
  16148. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16149. // - this default implementation will create a temporary TSQLRecord instance
  16150. // with the supplied Values[], and will call overloaded UpdateOne() method
  16151. function UpdateOne(ID: TID; const Values: TSQLVarDynArray): boolean; overload; virtual;
  16152. end;
  16153. /// class able to handle a O(1) hashed-based search of a property in a TList
  16154. // - used e.g. to hash TSQLRestStorageInMemory field values
  16155. TListFieldHash = class(TObjectHash)
  16156. protected
  16157. fValues: TList;
  16158. fField: integer;
  16159. fProp: TSQLPropInfo;
  16160. fCaseInsensitive: boolean;
  16161. /// overridden method to hash an item
  16162. function Hash(Item: TObject): cardinal; override;
  16163. /// overridden method to compare two items
  16164. function Compare(Item1,Item2: TObject): boolean; override;
  16165. /// overridden method to get an item
  16166. // - shall return nil if Index is out of range (e.g. >= Count)
  16167. // - will be called e.g. by Find() with Compare() to avoid collision
  16168. function Get(Index: integer): TObject; override;
  16169. /// overridden method to retrieve the number of items
  16170. function Count: integer; override;
  16171. public
  16172. /// initialize a hash for a record array field
  16173. // - aFieldIndex/aField parameters correspond to the indexed field (e.g.
  16174. // "stored AS_UNIQUE" published property)
  16175. // - if CaseInsensitive is TRUE, will apply NormToUpper[] 8 bits uppercase,
  16176. // handling RawUTF8 properties just like the SYSTEMNOCASE collation
  16177. constructor Create(aValues: TList; aField: TSQLPropInfo; aCaseInsensitive: boolean);
  16178. /// search one item using slow list browsing
  16179. function Scan(Item: TObject; ListCount: integer): integer; override;
  16180. /// the corresponding field index in the TSQLRecord
  16181. property FieldIndex: integer read fField;
  16182. /// the corresponding field RTTI
  16183. property Field: TSQLPropInfo read fProp;
  16184. /// if the string comparison shall be case-insensitive
  16185. property CaseInsensitive: boolean read fCaseInsensitive;
  16186. end;
  16187. /// REST storage with direct access to a TObjectList memory-stored table
  16188. // - store the associated TSQLRecord values in a TObjectList
  16189. // - handle one TSQLRecord per TSQLRestStorageInMemory instance
  16190. // - must be registered individualy in a TSQLRestServer to access data from a
  16191. // common client, by using the TSQLRestServer.StaticDataCreate method:
  16192. // it allows an unique access for both SQLite3 and Static databases
  16193. // - handle basic REST commands, no SQL interpreter is implemented: only
  16194. // valid SQL command is "SELECT Field1,Field2 FROM Table WHERE ID=120;", i.e
  16195. // a one Table SELECT with one optional "WHERE fieldname = value" statement;
  16196. // if used within a TSQLVirtualTableJSON, you'll be able to handle any kind of
  16197. // SQL statement (even joined SELECT or such) with this memory-stored database
  16198. // - our TSQLRestStorage database engine is very optimized and is a lot
  16199. // faster than SQLite3 for such queries - but its values remain in RAM,
  16200. // therefore it is not meant to deal with more than 100,000 rows
  16201. // - data can be stored and retrieved from a file (JSON format is used by
  16202. // default, if BinaryFile parameter is left to false; a proprietary compressed
  16203. // binary format can be used instead) if a file name is supplied at creating
  16204. // the TSQLRestStorageInMemory instance
  16205. TSQLRestStorageInMemory = class(TSQLRestStorageRecordBased)
  16206. protected
  16207. fValue: TObjectList;
  16208. fFileName: TFileName;
  16209. /// true if IDs are sorted (which is the default behavior of this class),
  16210. // for fastest ID2Index() by using a binary search algorithm
  16211. fIDSorted: boolean;
  16212. fCommitShouldNotUpdateFile: boolean;
  16213. fBinaryFile: boolean;
  16214. fExpandedJSON: boolean;
  16215. fSearchRec: TSQLRecord; // temporary record to store the searched value
  16216. fBasicUpperSQLSelect: array[boolean] of RawUTF8;
  16217. fUniqueFields: TObjectList;
  16218. function UniqueFieldsUpdateOK(aRec: TSQLRecord; aUpdateIndex: integer): boolean;
  16219. function UniqueFieldHash(aFieldIndex: integer): TListFieldHash;
  16220. function GetCount: integer;
  16221. function GetItem(Index: integer): TSQLRecord; {$ifdef HASINLINE}inline;{$endif}
  16222. function GetListPtr: PPointerArray; {$ifdef HASINLINE}inline;{$endif}
  16223. function GetID(Index: integer): TID;
  16224. procedure SetFileName(const aFileName: TFileName);
  16225. procedure SetBinaryFile(aBinary: boolean);
  16226. procedure GetJSONValuesEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  16227. procedure AddIntegerDynArrayEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  16228. procedure DoNothingEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  16229. procedure DoInstanceEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  16230. procedure DoIndexEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  16231. procedure DoCopyEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  16232. /// used to create the JSON content from a SELECT parsed command
  16233. // - WhereField index follows FindWhereEqual / TSynTableStatement.WhereField
  16234. // - returns the number of data row added (excluding field names)
  16235. // - this method is very fast and optimized (for search and JSON serializing)
  16236. function GetJSONValues(Stream: TStream; Expand: boolean;
  16237. Stmt: TSynTableStatement): PtrInt;
  16238. /// TSQLRestServer.URI use it for Static.EngineList to by-pass virtual table
  16239. // - overridden method to handle basic queries as handled by EngineList()
  16240. function AdaptSQLForEngineList(var SQL: RawUTF8): boolean; override;
  16241. /// overridden methods for direct in-memory database engine thread-safe process
  16242. function EngineRetrieve(TableModelIndex: Integer; ID: TID): RawUTF8; override;
  16243. function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
  16244. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  16245. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  16246. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  16247. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  16248. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  16249. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  16250. const IDs: TIDDynArray): boolean; override;
  16251. function EngineExecute(const aSQL: RawUTF8): boolean; override;
  16252. public
  16253. /// initialize the table storage data, reading it from a file if necessary
  16254. // - data encoding on file is UTF-8 JSON format by default, or
  16255. // should be some binary format if aBinaryFile is set to true
  16256. constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer;
  16257. const aFileName: TFileName = ''; aBinaryFile: boolean=false); reintroduce; virtual;
  16258. /// free used memory
  16259. // - especially release all fValue[] instances
  16260. destructor Destroy; override;
  16261. /// clear all the values of this table
  16262. // - will reset the associated database file, if any
  16263. procedure DropValues;
  16264. /// load the values from JSON data
  16265. procedure LoadFromJSON(const aJSON: RawUTF8); overload;
  16266. /// load the values from JSON data
  16267. procedure LoadFromJSON(JSONBuffer: PUTF8Char; JSONBufferLen: integer); overload;
  16268. /// save the values into JSON data
  16269. function SaveToJSON(Expand: Boolean): RawUTF8; overload;
  16270. /// save the values into JSON data
  16271. procedure SaveToJSON(Stream: TStream; Expand: Boolean); overload;
  16272. /// load the values from binary file/stream
  16273. // - the binary format is a custom compressed format (using our SynLZ fast
  16274. // compression algorithm), with variable-length record storage
  16275. // - the binary content is first checked for consistency, before loading
  16276. // - warning: the field layout should be the same at SaveToBinary call;
  16277. // for instance, it won't be able to read a file content with a renamed
  16278. // or modified field type
  16279. // - will return false if the binary content is invalid
  16280. function LoadFromBinary(Stream: TStream): boolean; overload;
  16281. /// load the values from binary data
  16282. // - uses the same compressed format as the overloaded stream/file method
  16283. // - will return false if the binary content is invalid
  16284. function LoadFromBinary(const Buffer: RawByteString): boolean; overload;
  16285. /// load the values from binary resource
  16286. // - the resource name is expected to be the TSQLRecord class name,
  16287. // with a resource type of 10
  16288. // - uses the same compressed format as the overloaded stream/file method
  16289. procedure LoadFromResource(ResourceName: string='');
  16290. /// save the values into a binary file/stream
  16291. // - the binary format is a custom compressed format (using our SynLZ fast
  16292. // compression algorithm), with variable-length record storage: e.g. a 27 KB
  16293. // Dali1.json content is stored into a 6 KB Dali2.data file
  16294. // (this data has a text redundant field content in its FirstName field);
  16295. // 502 KB People.json content is stored into a 92 KB People.data file
  16296. // - returns the number of bytes written into Stream
  16297. function SaveToBinary(Stream: TStream): integer; overload;
  16298. /// save the values into a binary buffer
  16299. // - uses the same compressed format as the overloaded stream/file method
  16300. function SaveToBinary: RawByteString; overload;
  16301. /// if file was modified, the file is updated on disk
  16302. // - this method is called automaticaly when the TSQLRestStorage
  16303. // instance is destroyed: should should want to call in in some cases,
  16304. // in order to force the data to be saved regularly
  16305. // - do nothing if the table content was not modified
  16306. // - will write JSON content by default, or binary content if BinaryFile
  16307. // property was set to true
  16308. procedure UpdateFile;
  16309. /// will reload all content from the current disk file
  16310. // - any not saved modification will be lost (e.g. if Updatefile has not
  16311. // been called since)
  16312. procedure ReloadFromFile;
  16313. /// retrieve the index in Items[] of a particular ID
  16314. // - return -1 if this ID was not found
  16315. // - use fast binary search algorithm (since Items[].ID should be increasing)
  16316. function IDToIndex(ID: TID): integer;
  16317. /// manual Add of a TSQLRecord
  16318. // - returns the ID created on success
  16319. // - returns -1 on failure (not UNIQUE field value e.g.)
  16320. // - on success, the Rec instance is added to the Values[] list: caller
  16321. // doesn't need to Free it
  16322. // - warning: this method should be protected via StorageLock/StorageUnlock
  16323. function AddOne(Rec: TSQLRecord; ForceID: boolean; const SentData: RawUTF8): TID; override;
  16324. /// manual Retrieval of a TSQLRecord field values
  16325. // - an instance of the associated static class is created, and filled with
  16326. // the actual properties values
  16327. // - and all its properties are filled from the Items[] values
  16328. // - caller can modify these properties, then use UpdateOne() if the changes
  16329. // have to be stored inside the Items[] list
  16330. // - calller must always free the returned instance
  16331. // - returns NIL if any error occured, e.g. if the supplied aID was incorrect
  16332. // - method available since a TSQLRestStorage instance may be created
  16333. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16334. function GetOne(aID: TID): TSQLRecord; override;
  16335. /// manual Update of a TSQLRecord field values
  16336. // - Rec.ID specifies which record is to be updated
  16337. // - will update all properties, including BLOB fields and such
  16338. // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID)
  16339. // - method available since a TSQLRestStorage instance may be created
  16340. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16341. function UpdateOne(Rec: TSQLRecord; const SentData: RawUTF8): boolean; override;
  16342. /// manual Update of a TSQLRecord field values from a TSQLVar array
  16343. // - will update all properties, including BLOB fields and such
  16344. // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID)
  16345. // - method available since a TSQLRestStorage instance may be created
  16346. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16347. function UpdateOne(ID: TID; const Values: TSQLVarDynArray): boolean; override;
  16348. /// direct deletion of a TSQLRecord, from its index in Values[]
  16349. // - warning: this method should be protected via StorageLock/StorageUnlock
  16350. function DeleteOne(aIndex: integer): boolean; virtual;
  16351. /// overridden method for direct in-memory database engine call
  16352. // - made public since a TSQLRestStorage instance may be created
  16353. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16354. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  16355. /// overridden method for direct in-memory database engine call
  16356. // - made public since a TSQLRestStorage instance may be created
  16357. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16358. function EngineUpdateField(TableModelIndex: integer;
  16359. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  16360. /// overridden method for direct in-memory database engine call
  16361. // - made public since a TSQLRestStorage instance may be created
  16362. // stand-alone, i.e. without any associated Model/TSQLRestServer
  16363. function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  16364. const FieldName: RawUTF8; Increment: Int64): boolean; override;
  16365. /// overridden method for direct in-memory database engine call
  16366. function UpdateBlobFields(Value: TSQLRecord): boolean; override;
  16367. /// overridden method for direct in-memory database engine call
  16368. function RetrieveBlobFields(Value: TSQLRecord): boolean; override;
  16369. /// overridden method for direct in-memory database engine call
  16370. function TableRowCount(Table: TSQLRecordClass): Int64; override;
  16371. /// overridden method for direct in-memory database engine call
  16372. function TableHasRows(Table: TSQLRecordClass): boolean; override;
  16373. /// search for a field value, according to its SQL content representation
  16374. // - return true on success (i.e. if some values have been added to ResultID)
  16375. // - store the results into the ResultID dynamic array
  16376. // - faster than OneFieldValues method, which creates a temporary JSON content
  16377. function SearchField(const FieldName, FieldValue: RawUTF8;
  16378. out ResultID: TIDDynArray): boolean; override;
  16379. /// search for a field value, according to its SQL content representation
  16380. // - return the found TSQLRecord on success, nil if none did match
  16381. // - warning: it returns a reference to one item of the unlocked internal
  16382. // list, so you should NOT use this on a read/write table, but rather
  16383. // use the slightly slower but safer SearchCopy() method or make explicit
  16384. // ! StorageLock ... try ... SearchInstance ... finally StorageUnlock end
  16385. function SearchInstance(const FieldName, FieldValue: RawUTF8): pointer;
  16386. /// search for a field value, according to its SQL content representation
  16387. // - return the found TSQLRecord index on success, -1 if none did match
  16388. // - warning: it returns a reference to the current index of the unlocked
  16389. // internal list, so you should NOT use without StorageLock/StorageUnlock
  16390. function SearchIndex(const FieldName, FieldValue: RawUTF8): integer;
  16391. /// search for a field value, according to its SQL content representation
  16392. // - return a copy of the found TSQLRecord on success, nil if no match
  16393. // - you should use SearchCopy() instead of SearchInstance(), unless you
  16394. // are sure that the internal TSQLRecord list won't change
  16395. function SearchCopy(const FieldName, FieldValue: RawUTF8): pointer;
  16396. /// search and count for a field value, according to its SQL content representation
  16397. // - return the number of found entries on success, 0 if it was not found
  16398. function SearchCount(const FieldName, FieldValue: RawUTF8): integer;
  16399. /// search for a field value, according to its SQL content representation
  16400. // - call the supplied OnFind event on match
  16401. // - returns the number of found entries
  16402. // - is just a wrapper around FindWhereEqual() with StorageLock protection
  16403. function SearchEvent(const FieldName, FieldValue: RawUTF8;
  16404. OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: integer): integer;
  16405. /// optimized search of WhereValue in WhereField (0=RowID,1..=RTTI)
  16406. // - will use fast O(1) hash for fUniqueFields[] fields
  16407. // - will use SYSTEMNOCASE case-insensitive search for text values, unless
  16408. // CaseInsensitive is set to FALSE
  16409. // - warning: this method should be protected via StorageLock/StorageUnlock
  16410. function FindWhereEqual(WhereField: integer; const WhereValue: RawUTF8;
  16411. OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: integer;
  16412. CaseInsensitive: boolean=true): PtrInt; overload;
  16413. /// optimized search of WhereValue in a field, specified by name
  16414. // - will use fast O(1) hash for fUniqueFields[] fields
  16415. // - will use SYSTEMNOCASE case-insensitive search for text values, unless
  16416. // CaseInsensitive is set to FALSE
  16417. // - warning: this method should be protected via StorageLock/StorageUnlock
  16418. function FindWhereEqual(const WhereFieldName, WhereValue: RawUTF8;
  16419. OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: integer;
  16420. CaseInsensitive: boolean=true): PtrInt; overload;
  16421. /// search the maximum value of a given column
  16422. // - would only handle integer/Int64 kind of column
  16423. function FindMax(WhereField: integer; out max: Int64): boolean;
  16424. /// execute a method on every TSQLRecord item
  16425. // - the loop execution will be protected via StorageLock/StorageUnlock
  16426. procedure ForEach(WillModifyContent: boolean;
  16427. OnEachProcess: TFindWhereEqualEvent; Dest: pointer);
  16428. /// read-only access to the TSQLRecord values, storing the data
  16429. // - this returns directly the item class instance stored in memory: if you
  16430. // change the content, it will affect the internal data - so for instance
  16431. // DO NOT change the ID values, unless you may have unexpected behavior
  16432. // - warning: this method should be protected via StorageLock/StorageUnlock
  16433. property Items[Index: integer]: TSQLRecord read GetItem; default;
  16434. /// direct access to the memory of the internal fValues[] array
  16435. // - Items[] is preferred, since it would check the index, but is slightly
  16436. // slower, e.g. in a loop or after a IDToIndex() call
  16437. // - warning: this method should be protected via StorageLock/StorageUnlock
  16438. property ListPtr: PPointerArray read GetListPtr;
  16439. /// read-only access to the ID of a TSQLRecord values
  16440. property ID[Index: integer]: TID read GetID;
  16441. published
  16442. /// read only access to the file name specified by constructor
  16443. // - you can call the TSQLRestServer.StaticDataCreate method to
  16444. // update the file name of an already instanciated static table
  16445. // - if you change manually the file name from this property, the storage
  16446. // would be marked as "modified" so that UpdateFile would save the content
  16447. property FileName: TFileName read fFileName write SetFileName;
  16448. /// if set to true, file content on disk will expect binary format
  16449. // - default format on disk is JSON but can be overridden at constructor call
  16450. // - binary format should be more efficient in term of speed and disk usage,
  16451. // but can be proprietary
  16452. // - if you change manually the file format from this property, the storage
  16453. // would be marked as "modified" so that UpdateFile would save the content
  16454. property BinaryFile: boolean read fBinaryFile write SetBinaryFile;
  16455. // JSON writing, can set if the format should be expanded or not
  16456. // - by default, the JSON will be in the custom non-expanded format,
  16457. // to save disk space and time
  16458. // - you can force the JSON to be emitted as an array of objects,
  16459. // e.g. for better human friendliness (reading and modification)
  16460. property ExpandedJSON: boolean read fExpandedJSON write fExpandedJSON;
  16461. /// set this property to TRUE if you want the COMMIT statement not to
  16462. // update the associated TSQLVirtualTableJSON
  16463. property CommitShouldNotUpdateFile: boolean read fCommitShouldNotUpdateFile
  16464. write fCommitShouldNotUpdateFile;
  16465. /// read-only access to the number of TSQLRecord values
  16466. property Count: integer read GetCount;
  16467. end;
  16468. /// a dynamic array of TSQLRestStorageInMemory instances
  16469. // - used e.g. by TSQLRestServerFullMemory
  16470. TSQLRestStorageInMemoryDynArray = array of TSQLRestStorageInMemory;
  16471. /// REST storage with direct access to a memory database, to be used as
  16472. // an external SQLite3 Virtual table
  16473. // - this is the kind of in-memory table expected by TSQLVirtualTableJSON,
  16474. // in order to be consistent with the internal DB cache
  16475. TSQLRestStorageInMemoryExternal = class(TSQLRestStorageInMemory)
  16476. public
  16477. /// initialize the table storage data, reading it from a file if necessary
  16478. // - data encoding on file is UTF-8 JSON format by default, or
  16479. // should be some binary format if aBinaryFile is set to true
  16480. constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer;
  16481. const aFileName: TFileName = ''; aBinaryFile: boolean=false); override;
  16482. /// this overridden method will notify the Owner when the internal DB content
  16483. // is known to be invalid
  16484. // - by default, all REST/CRUD requests and direct SQL statements are
  16485. // scanned and identified as potentially able to change the internal SQL/JSON
  16486. // cache used at SQLite3 database level; but TSQLVirtualTableJSON virtual
  16487. // tables could flush the database content without proper notification
  16488. // - this overridden implementation will call Owner.FlushInternalDBCache
  16489. procedure StorageLock(WillModifyContent: boolean); override;
  16490. end;
  16491. /// REST storage with redirection to another REST instance
  16492. // - allows redirection of all CRUD operations for a table to another
  16493. // TSQLRest instance, may be a remote TSQLRestClient or a TSQLRestServer
  16494. // - will be used by TSQLRestServer.RemoteDataCreate() method
  16495. TSQLRestStorageRemote = class(TSQLRestStorage)
  16496. protected
  16497. fRemoteRest: TSQLRest;
  16498. fRemoteTableIndex: integer;
  16499. function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
  16500. function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
  16501. function EngineExecute(const aSQL: RawUTF8): boolean; override;
  16502. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  16503. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  16504. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  16505. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  16506. const IDs: TIDDynArray): boolean; override;
  16507. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  16508. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  16509. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  16510. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  16511. function EngineUpdateField(TableModelIndex: integer;
  16512. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  16513. function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  16514. const FieldName: RawUTF8; Increment: Int64): boolean; override;
  16515. public
  16516. /// initialize the table storage redirection
  16517. // - you should not have to use this constructor, but rather the
  16518. // TSQLRestServer.RemoteDataCreate() method which will create and register
  16519. // one TSQLRestStorageRemote instance
  16520. constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer;
  16521. aRemoteRest: TSQLRest); reintroduce; virtual;
  16522. /// the remote ORM instance used for data persistence
  16523. // - may be a TSQLRestClient or a TSQLRestServer instance
  16524. property RemoteRest: TSQLRest read fRemoteRest;
  16525. end;
  16526. /// defines how TSQLRestStorageShard would handle its partioned process
  16527. TSQLRestStorageShardOption = (ssoNoUpdate, ssoNoUpdateButLastShard,
  16528. ssoNoDelete, ssoNoDeleteButLastShard, ssoNoBatch,
  16529. ssoNoList, ssoNoExecute, ssoNoUpdateField, ssoNoConsolidateAtDestroy);
  16530. /// how TSQLRestStorageShard would handle its partioned process
  16531. TSQLRestStorageShardOptions = set of TSQLRestStorageShardOption;
  16532. /// abstract REST storage with redirection to several REST instances, implementing
  16533. // range ID partitioning for horizontal scaling
  16534. // - such database shards would allow to scale with typical BigData storage
  16535. // - this storage would add items on a server, initializing a new server
  16536. // when the ID reached a defined range
  16537. // - it would maintain a list of previous storages, then redirect reading and
  16538. // updates to the server managing this ID (if possible - older shards may
  16539. // be deleted/ignored to release resources)
  16540. // - inherited class should override InitShards/InitNewShard to customize the
  16541. // kind of TSQLRest instances to be used for each shard (which may be local
  16542. // or remote, a SQLite3 engine or an external SQL/NoSQL database)
  16543. // - see inherited TSQLRestStorageShardDB as defined in mORMotSQLite3.pas
  16544. TSQLRestStorageShard = class(TSQLRestStorage)
  16545. protected
  16546. fShardRange: TID;
  16547. fLastID: TID;
  16548. fOptions: TSQLRestStorageShardOptions;
  16549. fShards: array of TSQLRest;
  16550. fShardLast: cardinal;
  16551. fShardLastID: TID;
  16552. fShardNextID: TID;
  16553. fShardTableIndex: TIntegerDynArray;
  16554. fShardBatch: array of TSQLRestBatch;
  16555. // would set Shards[],fShardLast,fShardLastID, nil if not available any more
  16556. procedure InitShards; virtual; abstract;
  16557. // should always return non nil shard to contain new added IDs
  16558. function InitNewShard: TSQLRest; virtual; abstract;
  16559. procedure InternalAddNewShard;
  16560. function InternalShardBatch(ShardIndex: integer): TSQLRestBatch;
  16561. // overriden methods which would handle all ORM process
  16562. function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
  16563. function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
  16564. function EngineExecute(const aSQL: RawUTF8): boolean; override;
  16565. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  16566. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  16567. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  16568. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  16569. const IDs: TIDDynArray): boolean; override;
  16570. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  16571. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  16572. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  16573. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  16574. function EngineUpdateField(TableModelIndex: integer;
  16575. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  16576. function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  16577. const FieldName: RawUTF8; Increment: Int64): boolean; override;
  16578. function InternalBatchStart(Method: TSQLURIMethod;
  16579. BatchOptions: TSQLRestBatchOptions): boolean; override;
  16580. procedure InternalBatchStop; override;
  16581. public
  16582. /// initialize the table storage redirection for sharding
  16583. // - you should not have to use this constructor, but e.g.
  16584. // TSQLRestStorageShardDB.Create on a main TSQLRestServer.StaticDataAdd()
  16585. // - the supplied aShardRange should be < 1000 - and once set, you should NOT
  16586. // change this value on an existing shard, unless process would be broken
  16587. constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer;
  16588. aShardRange: TID; aOptions: TSQLRestStorageShardOptions); reintroduce; virtual;
  16589. /// finalize the table storage, including Shards[] instances
  16590. destructor Destroy; override;
  16591. /// you may call this method sometimes to consolidate the sharded data
  16592. // - may e.g. merge/compact shards, depending on scaling expectations
  16593. // - also called by Destroy - do nothing by default
  16594. procedure ConsolidateShards; virtual;
  16595. /// remove a shard database from the current set
  16596. // - it would allow e.g. to delete a *.dbs file at runtime, without
  16597. // restarting the server
  16598. // - this default implementation would free and nil fShard[aShardIndex]
  16599. procedure RemoveShard(aShardIndex: integer); virtual;
  16600. /// retrieve the ORM shard instance corresponding to an ID
  16601. // - may return false if the correspondig shard is not available any more
  16602. // - may return true, and a TSQLRestHookClient or a TSQLRestHookServer instance
  16603. // with its associated index in TSQLRest.Model.Tables[]
  16604. function ShardFromID(aID: TID; out aShardTableIndex: integer;
  16605. out aShard: TSQLRest; aOccasion: TSQLOccasion=soSelect;
  16606. aShardIndex: PInteger=nil): boolean; virtual;
  16607. /// get the row count of a specified table
  16608. function TableRowCount(Table: TSQLRecordClass): Int64; override;
  16609. /// check if there is some data rows in a specified table
  16610. function TableHasRows(Table: TSQLRecordClass): boolean; override;
  16611. published
  16612. /// how much IDs should store each ORM shard instance
  16613. // - once set, you should NEVER change this value on an existing shard,
  16614. // otherwise the whole ID partition would fail
  16615. // - each shard would hold [ShardIndex*ShardRange..(ShardIndex+1)*ShardRange-1] IDs
  16616. property ShardRange: TID read fShardRange;
  16617. /// defines how this instance would handle its sharding process
  16618. // - by default, update/delete operations or per ID retrieval would take
  16619. // place on all shards, whereas EngineList and EngineExecute would only run
  16620. // only on the latest shard (to save resources)
  16621. property Options: TSQLRestStorageShardOptions read fOptions write fOptions;
  16622. end;
  16623. /// class metadata of a Sharding storage engine
  16624. TSQLRestStorageShardClass = class of TSQLRestStorageShard;
  16625. /// a REST server using only in-memory tables
  16626. // - this server will use TSQLRestStorageInMemory instances to handle
  16627. // the data in memory, and optionally persist the data on disk as JSON or
  16628. // binary files
  16629. // - so it will not handle all SQL requests, just basic CRUD commands on
  16630. // separated tables
  16631. // - at least, it will compile as a TSQLRestServer without complaining for
  16632. // pure abstract methods; it can be used to host some services if database
  16633. // and ORM needs are basic (e.g. if only authentication and CRUD are needed),
  16634. // without the need to link the SQLite3 engine
  16635. TSQLRestServerFullMemory = class(TSQLRestServer)
  16636. protected
  16637. fFileName: TFileName;
  16638. fBinaryFile: Boolean;
  16639. fStaticDataCount: cardinal;
  16640. fStorage: TSQLRestStorageInMemoryDynArray;
  16641. function GetStorage(aTable: TSQLRecordClass): TSQLRestStorageInMemory;
  16642. /// overridden methods which will call fStorage[TableModelIndex] directly
  16643. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  16644. function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
  16645. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  16646. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  16647. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  16648. const IDs: TIDDynArray): boolean; override;
  16649. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  16650. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  16651. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  16652. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  16653. function EngineUpdateField(TableModelIndex: integer;
  16654. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  16655. function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  16656. const FieldName: RawUTF8; Increment: Int64): boolean; override;
  16657. /// overridden methods which will return error (no main DB here)
  16658. function MainEngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  16659. function MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
  16660. function MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; override;
  16661. function MainEngineUpdate(TableModelIndex: integer; aID: TID; const SentData: RawUTF8): boolean; override;
  16662. function MainEngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  16663. function MainEngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  16664. const IDs: TIDDynArray): boolean; override;
  16665. function MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  16666. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  16667. function MainEngineUpdateBlob(TableModelIndex: integer; aID: TID;
  16668. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  16669. function MainEngineUpdateField(TableModelIndex: integer;
  16670. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  16671. function MainEngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  16672. const FieldName: RawUTF8; Increment: Int64): boolean; override;
  16673. // method not implemented: always return false
  16674. function EngineExecute(const aSQL: RawUTF8): boolean; override;
  16675. constructor RegisteredClassCreateFrom(aModel: TSQLModel;
  16676. aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); override;
  16677. public
  16678. /// initialize an in-memory REST server with no database file
  16679. constructor Create(aModel: TSQLModel; aHandleUserAuthentication: boolean=false); overload; override;
  16680. /// initialize an in-memory REST server with a database file
  16681. // - all classes of the model will be created as TSQLRestStorageInMemory
  16682. // - then data persistence will be initialized using aFileName, but no
  16683. // file will be written to disk, unless you call explicitly UpdateToFile
  16684. // - if aFileName is left void (''), data will not be persistent
  16685. constructor Create(aModel: TSQLModel; const aFileName: TFileName;
  16686. aBinaryFile: boolean=false; aHandleUserAuthentication: boolean=false); reintroduce; overload; virtual;
  16687. /// initialize an in-memory REST server with a temporary Database Model,
  16688. // and optional authentication by a single user
  16689. // - a Model will be created with supplied tables, and owned by the server
  16690. // - if aUserName is set, authentication will be enabled, and the supplied
  16691. // credentials will be used to authenticate a single user, member of the
  16692. // 'Supervisor' group - in this case, aHashedPassword value should match
  16693. // TSQLAuthUser.PasswordHashHexa expectations
  16694. constructor CreateWithOwnedAuthenticatedModel(
  16695. const Tables: array of TSQLRecordClass; const aUserName, aHashedPassword: RawUTF8;
  16696. aRoot: RawUTF8='root');
  16697. /// finalize the REST server
  16698. // - this overridden destructor will write any modification on file (if
  16699. // needed), and release all used memory
  16700. destructor Destroy; override;
  16701. /// save the TSQLRestFullMemory properties into a persistent storage object
  16702. // - CreateFrom() will expect Definition.ServerName to store the FileName,
  16703. // and use binary storage if Definition.DatabaseName is not void
  16704. procedure DefinitionTo(Definition: TSynConnectionDefinition); override;
  16705. /// Missing tables are created if they don't exist yet for every TSQLRecord
  16706. // class of the Database Model
  16707. // - you must call explicitely this before having called StaticDataCreate()
  16708. // - all table description (even Unique feature) is retrieved from the Model
  16709. // - this method also create additional fields, if the TSQLRecord definition
  16710. // has been modified; only field adding is available, field renaming or
  16711. // field deleting are not allowed in the FrameWork (in such cases, you must
  16712. // create a new TSQLRecord type)
  16713. procedure CreateMissingTables(user_version: cardinal=0;
  16714. Options: TSQLInitializeTableOptions=[]); override;
  16715. /// load the content from the specified file name
  16716. // - do nothing if file name was not assigned
  16717. procedure LoadFromFile; virtual;
  16718. /// load the content from the supplied resource
  16719. procedure LoadFromStream(aStream: TStream); virtual;
  16720. /// write any modification into file
  16721. // - do nothing if file name was not assigned
  16722. procedure UpdateToFile; virtual;
  16723. /// clear all internal TObjectList content
  16724. procedure DropDatabase; virtual;
  16725. /// direct access to the storage TObjectList storage instances
  16726. // - you can then access to Storage[Table].Count and Storage[Table].Items[]
  16727. property Storage[aTable: TSQLRecordClass]: TSQLRestStorageInMemory read GetStorage;
  16728. /// direct access to the storage TObjectList storage instances
  16729. // - you can then access via Storage[TableIndex].Count and Items[]
  16730. property Storages: TSQLRestStorageInMemoryDynArray read fStorage;
  16731. published
  16732. /// the file name used for data persistence
  16733. property FileName: TFileName read fFileName write fFileName;
  16734. /// set if the file content is to be compressed binary, or standard JSON
  16735. // - it will use TSQLRestStorageInMemory LoadFromJSON/LoadFromBinary
  16736. // SaveToJSON/SaveToBinary methods for optimized storage
  16737. property BinaryFile: Boolean read fBinaryFile write fBinaryFile;
  16738. published
  16739. /// this method-base service will be accessible from ModelRoot/Flush URI,
  16740. // and will write any modification into file
  16741. // - method parameters signature matches TSQLRestServerCallBack type
  16742. // - do nothing if file name was not assigned
  16743. // - can be used from a remote client to ensure that any Add/Update/Delete
  16744. // will be stored to disk, via
  16745. // ! aClient.CallBackPut('Flush','',dummy)
  16746. procedure Flush(Ctxt: TSQLRestServerURIContext);
  16747. end;
  16748. /// a REST server using another TSQLRest instance for all its ORM process
  16749. // - this server will use an internal TSQLRest instance to handle all ORM
  16750. // operations (i.e. access to objects) - e.g. TSQLRestClient for remote access
  16751. // - it can be used e.g. to host some services on a stand-alone server, with
  16752. // all ORM and data access retrieved from another server: it will allow to
  16753. // easily implement a proxy architecture (for instance, as a DMZ for
  16754. // publishing services, but letting ORM process stay out of scope)
  16755. // - for per-table redirection, consider using the TSQLRestStorageRemote class
  16756. // via a call to the TSQLRestServer.RemoteDataCreate() method
  16757. TSQLRestServerRemoteDB = class(TSQLRestServer)
  16758. protected
  16759. fRemoteRest: TSQLRest;
  16760. fRemoteTableIndex: TIntegerDynArray;
  16761. function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
  16762. function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
  16763. function EngineExecute(const aSQL: RawUTF8): boolean; override;
  16764. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  16765. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  16766. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  16767. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  16768. const IDs: TIDDynArray): boolean; override;
  16769. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  16770. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  16771. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  16772. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  16773. function EngineUpdateField(TableModelIndex: integer;
  16774. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  16775. function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
  16776. const FieldName: RawUTF8; Increment: Int64): boolean; override;
  16777. public
  16778. /// initialize a REST server associated to a given TSQLRest instance
  16779. // - the specified TSQLRest will be used for all ORM and data process
  16780. // - you could use a TSQLRestClient or a TSQLRestServer instance
  16781. // - the supplied TSQLRest.Model will be used for TSQLRestServerRemoteDB
  16782. // - note that the TSQLRest instance won't be freed - caller shall ensure
  16783. // that it will stay available at least until TSQLRestServerRemoteDB.Free
  16784. constructor Create(aRemoteRest: TSQLRest;
  16785. aHandleUserAuthentication: boolean=false); reintroduce; virtual;
  16786. /// this method is called internally after any successfull deletion to
  16787. // ensure relational database coherency
  16788. // - this overridden method will just return TRUE: in this remote access,
  16789. // true coherency will be performed on the ORM server side
  16790. function AfterDeleteForceCoherency(TableIndex: integer; aID: TID): boolean; override;
  16791. published
  16792. /// the remote ORM instance used for data persistence
  16793. // - may be a TSQLRestClient or a TSQLRestServer instance
  16794. property RemoteRest: TSQLRest read fRemoteRest;
  16795. end;
  16796. /// possible call parameters for TOnTableUpdate Event
  16797. TOnTableUpdateState = (tusPrepare, tusChanged, tusNoChange);
  16798. /// used by TSQLRestClientURI.UpdateFromServer() to let the client
  16799. // perform the rows update (for Marked[] e.g.)
  16800. TOnTableUpdate = procedure(aTable: TSQLTableJSON; State: TOnTableUpdateState) of object;
  16801. /// used by TSQLRestClientURI.Update() to let the client
  16802. // perform the record update (refresh associated report e.g.)
  16803. TOnRecordUpdate = procedure(Value: TSQLRecord) of object;
  16804. /// a generic REpresentational State Transfer (REST) client
  16805. // - is RESTful (i.e. URI) remotely implemented (TSQLRestClientURI e.g.)
  16806. // - is implemented for direct access to a database (TSQLRestClientDB e.g.)
  16807. TSQLRestClient = class(TSQLRest)
  16808. protected
  16809. fForceBlobTransfert: array of boolean;
  16810. fOnTableUpdate: TOnTableUpdate;
  16811. fOnRecordUpdate: TOnRecordUpdate;
  16812. function GetForceBlobTransfert: Boolean;
  16813. procedure SetForceBlobTransfert(Value: boolean);
  16814. function GetForceBlobTransfertTable(aTable: TSQLRecordClass): Boolean;
  16815. procedure SetForceBlobTransfertTable(aTable: TSQLRecordClass; aValue: Boolean);
  16816. /// get a member from its ID
  16817. // - implements REST GET collection
  16818. // - returns the data of this object as JSON
  16819. // - override this method for proper data retrieval from the database engine
  16820. // - this method must be implemented in a thread-safe manner
  16821. function ClientRetrieve(TableModelIndex: integer; ID: TID;
  16822. ForUpdate: boolean; var InternalState: cardinal; var Resp: RawUTF8): boolean; virtual; abstract;
  16823. /// this method is called before updating any record
  16824. // - should return FALSE to force no update
  16825. // - can be use to update some field values just before saving to the database
  16826. // (e.g. for digital signing purpose)
  16827. // - this default method just return TRUE (i.e. OK to update)
  16828. function BeforeUpdateEvent(Value: TSQLRecord): Boolean; virtual;
  16829. /// overridden method which will call ClientRetrieve()
  16830. function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
  16831. /// create a new member
  16832. // - implements REST POST collection
  16833. // - URI is 'ModelRoot/TableName' with POST method
  16834. // - if SendData is true, content of Value is sent to the server as JSON
  16835. // - if ForceID is true, client sends the Value.ID field to use this ID
  16836. // - server must return Status 201/HTML_CREATED on success
  16837. // - server must send on success an header entry with
  16838. // $ Location: ModelRoot/TableName/TableID
  16839. // - on success, returns the new ROWID value; on error, returns 0
  16840. // - on success, Value.ID is updated with the new ROWID
  16841. // - if aValue is TSQLRecordFTS3, Value.ID is stored to the virtual table
  16842. // - this overridden method will send BLOB fields, if ForceBlobTransfert is set
  16843. function InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits;
  16844. ForceID, DoNotAutoComputeFields: boolean): TID; override;
  16845. public
  16846. /// update a member
  16847. // - implements REST PUT collection
  16848. // - URI is 'ModelRoot/TableName/TableID' with PUT method
  16849. // - server must return Status 200/HTML_SUCCESS OK on success
  16850. // - this overridden method will call BeforeUpdateEvent and also update BLOB
  16851. // fields, if any ForceBlobTransfert is set and CustomFields=[]
  16852. function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[];
  16853. DoNotAutoComputeFields: boolean=false): boolean; override;
  16854. /// get a member from its ID
  16855. // - implements REST GET collection
  16856. // - URI is 'ModelRoot/TableName/TableID' with GET method
  16857. // - server must return Status 200/HTML_SUCCESS OK on success
  16858. // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock
  16859. // the corresponding record, then retrieve its content; caller has to call
  16860. // UnLock() method after Value usage, to release the record
  16861. function Retrieve(aID: TID; Value: TSQLRecord; ForUpdate: boolean=false): boolean; override;
  16862. /// get a member from its ID
  16863. // - implements REST GET collection
  16864. // - URI is 'ModelRoot/TableName/TableID' with GET method
  16865. // - returns true on server returned 200/HTML_SUCCESS OK success, false on error
  16866. // - set Refreshed to true if the content changed
  16867. function Refresh(aID: TID; Value: TSQLRecord; var Refreshed: boolean): boolean;
  16868. /// retrieve a list of members as a TSQLTable
  16869. // - implements REST GET collection
  16870. // - default SQL statement is 'SELECT ID FROM TableName;' (i.e. retrieve
  16871. // the list of all ID of this collection members)
  16872. // - optional SQLSelect parameter to change the returned fields
  16873. // as in 'SELECT SQLSelect FROM TableName;'
  16874. // - optional SQLWhere parameter to change the search range or ORDER
  16875. // as in 'SELECT SQLSelect FROM TableName WHERE SQLWhere;'
  16876. // - using inlined parameters via :(...): in SQLWhere is always a good idea
  16877. // - for one TClass, you should better use TSQLRest.MultiFieldValues()
  16878. function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8 = 'RowID';
  16879. const SQLWhere: RawUTF8 = ''): TSQLTableJSON; virtual; abstract;
  16880. /// retrieve a list of members as a TSQLTable
  16881. // - implements REST GET collection
  16882. // - in this version, the WHERE clause can be created with the same format
  16883. // as FormatUTF8() function, replacing all '%' chars with Args[] values
  16884. // - using inlined parameters via :(...): in SQLWhereFormat is always a good idea
  16885. // - for one TClass, you should better use TSQLRest.MultiFieldValues()
  16886. // - will call the List virtual method internaly
  16887. function ListFmt(const Tables: array of TSQLRecordClass;
  16888. const SQLSelect, SQLWhereFormat: RawUTF8; const Args: array of const): TSQLTableJSON; overload;
  16889. /// retrieve a list of members as a TSQLTable
  16890. // - implements REST GET collection
  16891. // - in this version, the WHERE clause can be created with the same format
  16892. // as FormatUTF8() function, replacing all '%' chars with Args[], and all '?'
  16893. // chars with Bounds[] (inlining them with :(...): and auto-quoting strings)
  16894. // - example of use:
  16895. // ! Table := ListFmt([TSQLRecord],'Name','ID=?',[],[aID]);
  16896. // - for one TClass, you should better use TSQLRest.MultiFieldValues()
  16897. // - will call the List virtual method internaly
  16898. function ListFmt(const Tables: array of TSQLRecordClass;
  16899. const SQLSelect, SQLWhereFormat: RawUTF8; const Args, Bounds: array of const): TSQLTableJSON; overload;
  16900. /// dedicated method used to retrieve matching IDs using a fast R-Tree index
  16901. // - a TSQLRecordRTree is associated to a TSQLRecord with a specified BLOB
  16902. // field, and will call TSQLRecordRTree BlobToCoord and ContainedIn virtual
  16903. // class methods to execute an optimized SQL query
  16904. // - will return all matching DataTable IDs in DataID[]
  16905. // - will generate e.g. the following statement
  16906. // $ SELECT MapData.ID From MapData, MapBox WHERE MapData.ID=MapBox.ID
  16907. // $ AND minX>=:(-81.0): AND maxX<=:(-79.6): AND minY>=:(35.0): AND :(maxY<=36.2):
  16908. // $ AND MapBox_in(MapData.BlobField,:('\uFFF0base64encoded-81,-79.6,35,36.2'):);
  16909. // when the following Delphi code is executed:
  16910. // ! aClient.RTreeMatch(TSQLRecordMapData,'BlobField',TSQLRecordMapBox,
  16911. // ! aMapData.BlobField,ResultID);
  16912. function RTreeMatch(DataTable: TSQLRecordClass;
  16913. const DataTableBlobFieldName: RawUTF8; RTreeTable: TSQLRecordRTreeClass;
  16914. const DataTableBlobField: RawByteString; var DataID: TIDDynArray): boolean;
  16915. /// begin a transaction (calls REST BEGIN Member)
  16916. // - by default, Client transaction will use here a pseudo session
  16917. // - in aClient-Server environment with multiple Clients connected at the
  16918. // same time, you should better use BATCH process, specifying a positive
  16919. // AutomaticTransactionPerRow parameter to BatchStart()
  16920. function TransactionBegin(aTable: TSQLRecordClass;
  16921. SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED): boolean; override;
  16922. /// end a transaction (calls REST END Member)
  16923. // - by default, Client transaction will use here a pseudo session
  16924. procedure Commit(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED;
  16925. RaiseException: boolean=false); override;
  16926. /// abort a transaction (calls REST ABORT Member)
  16927. // - by default, Client transaction will use here a pseudo session
  16928. procedure RollBack(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED); override;
  16929. /// access or initialize the internal IoC resolver, used for interface-based
  16930. // remote services, and more generaly any Services.Resolve() call
  16931. // - create and initialize the internal TServiceContainerClient if no
  16932. // service interface has been registered yet
  16933. // - may be used to inject some dependencies, which are not interface-based
  16934. // remote services, but internal IoC, without the ServiceRegister()
  16935. // or ServiceDefine() methods - e.g.
  16936. // ! aRest.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(aRest)],true);
  16937. function ServiceContainer: TServiceContainer; override;
  16938. /// if set to TRUE, all BLOB fields of all tables will be transferred
  16939. // between the Client and the remote Server
  16940. // - i.e. Add() Update() will use Blob-related RESTful PUT/POST request
  16941. // - i.e. Retrieve() will use Blob-related RESTful GET request
  16942. // - note that the Refresh method won't handle BLOB fields, even if this
  16943. // property setting is set to TRUE
  16944. // - by default, this property is set to FALSE, which setting will spare
  16945. // bandwidth and CPU
  16946. // - this property is global to all tables of the model - you can also use
  16947. // ForceBlobTransfertTable[] to force it for a particular table
  16948. property ForceBlobTransfert: boolean read GetForceBlobTransfert write SetForceBlobTransfert;
  16949. /// if set to TRUE for a specified table of the model, all BLOB fields of
  16950. // this tables will be transferred between the Client and the remote Server
  16951. // - i.e. Add() Update() will use BLOB-related RESTful PUT/POST request for
  16952. // this table
  16953. // - i.e. Retrieve() will use BLOB-related RESTful GET request for
  16954. // this table
  16955. // - note that the Refresh method won't handle BLOB fields, even if this
  16956. // property setting is set to TRUE
  16957. // - by default, all items of this property are set to FALSE, which
  16958. // setting will spare bandwidth and CPU
  16959. // - this property is particular to a given tables of the model - you can
  16960. // also use ForceBlobTransfert to force it for a all tables of this model
  16961. property ForceBlobTransfertTable[aTable: TSQLRecordClass]: Boolean
  16962. read GetForceBlobTransfertTable write SetForceBlobTransfertTable;
  16963. /// this Event is called by UpdateFromServer() to let the Client adapt to
  16964. // some rows update (for Marked[] e.g.)
  16965. property OnTableUpdate: TOnTableUpdate read fOnTableUpdate write fOnTableUpdate;
  16966. /// this Event is called by Update() to let the client
  16967. // perform the record update (refresh associated report e.g.)
  16968. property OnRecordUpdate: TOnRecordUpdate read fOnRecordUpdate write fOnRecordUpdate;
  16969. end;
  16970. /// used by TSQLRestClientURI.URI() to let the client ask for an User name
  16971. // and password, in order to retry authentication to the server
  16972. // - should return TRUE if aUserName and aPassword both contain some entered
  16973. // values to be sent for remote secure authentication
  16974. // - should return FALSE if the user pressed cancel or the number of Retry
  16975. // reached a defined limit
  16976. TOnAuthentificationFailed = function(Retry: integer;
  16977. var aUserName, aPassword: string; out aPasswordHashed: boolean): boolean of object;
  16978. /// called by TSQLRestClientURI.URI() when an error occurred
  16979. // - so that you may have a single entry point for all client-side issues
  16980. // - information would be available in Sender's LastErrorCode and
  16981. // LastErrorMessage properties
  16982. // - if the error comes from an Execption, it would be supplied as parameter
  16983. // - the REST context (if any) would be supplied within the Call parameter
  16984. TOnClientFailed = procedure(Sender: TSQLRestClientURI; E: Exception;
  16985. Call: PSQLRestURIParams) of object;
  16986. /// store information about registered interface callbacks
  16987. TSQLRestClientCallbackItem = record
  16988. /// the identifier of the callback, as sent to the server side
  16989. // - computed from TSQLRestClientURICallbacks.fCurrentID counter
  16990. ID: integer;
  16991. /// pointer typecast to the associated IInvokable variable
  16992. Instance: pointer;
  16993. //// information about the associated IInvokable
  16994. Factory: TInterfaceFactory;
  16995. /// set to TRUE if the instance was released from the server
  16996. ReleasedFromServer: boolean;
  16997. end;
  16998. /// points to information about registered interface callbacks
  16999. PSQLRestClientCallbackItem = ^TSQLRestClientCallbackItem;
  17000. /// store the references to active interface callbacks on a REST Client
  17001. TSQLRestClientCallbacks = class(TSynPersistentLocked)
  17002. protected
  17003. fCurrentID: integer;
  17004. function UnRegisterByIndex(index: integer): boolean;
  17005. public
  17006. /// the associated REST instance
  17007. Owner: TSQLRestClientURI;
  17008. /// how many callbacks are registered
  17009. Count: integer;
  17010. /// list of registered interface callbacks
  17011. List: array of TSQLRestClientCallbackItem;
  17012. /// initialize the storage list
  17013. constructor Create(aOwner: TSQLRestClientURI); reintroduce;
  17014. /// register a callback event interface instance from a new computed ID
  17015. function DoRegister(aInstance: pointer; aFactory: TInterfaceFactory): integer; overload;
  17016. /// register a callback event interface instance from its supplied ID
  17017. procedure DoRegister(aID: Integer; aInstance: pointer; aFactory: TInterfaceFactory); overload;
  17018. /// delete all callback events from the internal list, as specified by its instance
  17019. // - note that the same IInvokable instance may be registered for several IDs
  17020. function UnRegister(aInstance: pointer): boolean; overload;
  17021. /// find the index of the ID in the internal list
  17022. // - warning: this method should be called within Safe.Lock/Safe.Unlock
  17023. function FindIndex(aID: integer): integer;
  17024. /// find a matching callback
  17025. // - will call FindIndex(aItem.ID) within Safe.Lock/Safe.Unlock
  17026. // - returns TRUE if aItem.ID was found and aItem filled, FALSE otherwise
  17027. function FindEntry(var aItem: TSQLRestClientCallbackItem): boolean;
  17028. /// find a matching entry
  17029. // - will call FindIndex(aID) within Safe.Lock/Safe.Unlock
  17030. // - returns TRUE if aID was found and aInstance/aFactory set, FALSE otherwise
  17031. function FindAndRelease(aID: integer): boolean;
  17032. end;
  17033. /// a generic REpresentational State Transfer (REST) client with URI
  17034. // - URI are standard Collection/Member implemented as ModelRoot/TableName/TableID
  17035. // - handle RESTful commands GET POST PUT DELETE LOCK UNLOCK
  17036. TSQLRestClientURI = class(TSQLRestClient)
  17037. protected
  17038. fOnAuthentificationFailed: TOnAuthentificationFailed;
  17039. fOnSetUser: TNotifyEvent;
  17040. fMaximumAuthentificationRetry: Integer;
  17041. fRetryOnceOnTimeout: boolean;
  17042. fLastErrorCode: integer;
  17043. fLastErrorMessage: RawUTF8;
  17044. fLastErrorException: ExceptClass;
  17045. fBatchCurrent: TSQLRestBatch;
  17046. /// private values created by sucessfull SetUser() method
  17047. fSessionUser: TSQLAuthUser;
  17048. fSessionID: cardinal;
  17049. fSessionIDHexa8: RawUTF8;
  17050. fSessionPrivateKey: cardinal;
  17051. fSessionLastTick64: Int64;
  17052. fSessionAuthentication: TSQLRestServerAuthenticationClass;
  17053. fSessionHttpHeader: RawUTF8; // e.g. for TSQLRestServerAuthenticationHttpBasic
  17054. fSessionServer: RawUTF8;
  17055. fSessionVersion: RawUTF8;
  17056. fSessionData: RawByteString;
  17057. /// used to make the internal client-side process reintrant
  17058. fSafe: IAutoLocker;
  17059. fRemoteLogClass: TSynLog;
  17060. fRemoteLogOwnedByFamily: boolean;
  17061. fServicePublishOwnInterfaces: RawUTF8;
  17062. {$ifdef MSWINDOWS}
  17063. fServiceNotificationMethodViaMessages: record
  17064. Wnd: HWND;
  17065. Msg: UINT;
  17066. end;
  17067. {$endif}
  17068. {$ifndef LVCL} // SyncObjs.TEvent not available in LVCL yet
  17069. fBackgroundThread: TSynBackgroundThreadEvent;
  17070. fOnIdle: TOnIdleSynBackgroundThread;
  17071. fOnFailed: TOnClientFailed;
  17072. fRemoteLogThread: TObject; // private TRemoteLogThread
  17073. fFakeCallbacks: TSQLRestClientCallbacks;
  17074. function FakeCallbackRegister(Sender: TServiceFactoryClient;
  17075. const Method: TServiceMethod; const ParamInfo: TServiceMethodArgument;
  17076. ParamValue: Pointer): integer; virtual;
  17077. function FakeCallbackUnregister(Factory: TInterfaceFactory;
  17078. FakeCallbackID: integer; Instance: pointer): boolean; virtual;
  17079. procedure OnBackgroundProcess(Sender: TSynBackgroundThreadEvent;
  17080. ProcessOpaqueParam: pointer);
  17081. function GetOnIdleBackgroundThreadActive: boolean;
  17082. {$endif}
  17083. constructor RegisteredClassCreateFrom(aModel: TSQLModel;
  17084. aDefinition: TSynConnectionDefinition); override;
  17085. function GetCurrentSessionUserID: TID; override;
  17086. function InternalRemoteLogSend(const aText: RawUTF8): boolean;
  17087. procedure InternalNotificationMethodExecute(var Ctxt: TSQLRestURIParams); virtual;
  17088. procedure SetLastException(E: Exception=nil; ErrorCode: integer=HTML_BADREQUEST;
  17089. Call: PSQLRestURIParams=nil);
  17090. // register the user session to the TSQLRestClientURI instance
  17091. function SessionCreate(aAuth: TSQLRestServerAuthenticationClass;
  17092. var aUser: TSQLAuthUser; const aSessionKey: RawUTF8): boolean;
  17093. /// abstract method to be implemented with a local, piped or HTTP/1.1 provider
  17094. // - you can specify some POST/PUT data in Call.OutBody (leave '' otherwise)
  17095. // - return the execution result in Call.OutStatus
  17096. // - for clients, RestAccessRights is never used
  17097. procedure InternalURI(var Call: TSQLRestURIParams); virtual; abstract;
  17098. /// overridden protected method shall check if not connected to reopen it
  17099. // - shall return TRUE on success, FALSE on any connection error
  17100. function InternalCheckOpen: boolean; virtual; abstract;
  17101. /// overridden protected method shall force the connection to be closed,
  17102. // - a next call to InternalCheckOpen method shall re-open the connection
  17103. procedure InternalClose; virtual; abstract;
  17104. /// calls 'ModelRoot/TableName/TableID' with appropriate REST method
  17105. // - uses GET method if ForUpdate is false
  17106. // - uses LOCK method if ForUpdate is true
  17107. function URIGet(Table: TSQLRecordClass; ID: TID; var Resp: RawUTF8;
  17108. ForUpdate: boolean=false): Int64Rec;
  17109. // overridden methods
  17110. function ClientRetrieve(TableModelIndex: integer; ID: TID; ForUpdate: boolean;
  17111. var InternalState: cardinal; var Resp: RawUTF8): boolean; override;
  17112. function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
  17113. function EngineExecute(const SQL: RawUTF8): boolean; override;
  17114. function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
  17115. function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
  17116. function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
  17117. function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
  17118. const IDs: TIDDynArray): boolean; override;
  17119. function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  17120. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
  17121. function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  17122. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
  17123. function EngineUpdateField(TableModelIndex: integer;
  17124. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
  17125. function EngineBatchSend(Table: TSQLRecordClass; const Data: RawUTF8;
  17126. var Results: TIDDynArray; ExpectedResultsCount: integer): integer; override;
  17127. public
  17128. /// initialize REST client instance
  17129. constructor Create(aModel: TSQLModel); override;
  17130. /// release memory and close client connection
  17131. // - also unlock all still locked records by this client
  17132. destructor Destroy; override;
  17133. /// authenticate an User to the current connected Server
  17134. // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth()
  17135. // published method to create a session for this user, with our secure
  17136. // TSQLRestServerAuthenticationDefault authentication scheme
  17137. // - returns true on success
  17138. // - calling this method is optional, depending on your user right policy:
  17139. // your Server need to handle authentication
  17140. // - if saoUserByLogonOrID is defined in the server Options, aUserName may
  17141. // be a TSQLAuthUser.ID integer value and not a TSQLAuthUser.LogonName
  17142. // - on success, the SessionUser property map the logged user session on the
  17143. // server side
  17144. // - if aHashedPassword is TRUE, the aPassword parameter is expected to
  17145. // contain the already-hashed value, just as stored in PasswordHashHexa
  17146. // (i.e. SHA256('salt'+Value) as in TSQLAuthUser.SetPasswordPlain method)
  17147. // - if SSPIAUTH conditional is defined, and aUserName='', a Windows
  17148. // authentication will be performed via TSQLRestServerAuthenticationSSPI -
  17149. // in this case, aPassword will contain the SPN domain for Kerberos
  17150. // (otherwise NTLM will be used), and table TSQLAuthUser shall contain
  17151. // an entry for the logged Windows user, with the LoginName in form
  17152. // 'DomainName\UserName'
  17153. // - you can directly create the class method ClientSetUser() of a given
  17154. // TSQLRestServerAuthentication inherited class, if neither
  17155. // TSQLRestServerAuthenticationDefault nor TSQLRestServerAuthenticationSSPI
  17156. // match your need
  17157. function SetUser(const aUserName, aPassword: RawUTF8;
  17158. aHashedPassword: Boolean=false): boolean;
  17159. /// save the TSQLRestClientURI properties into a persistent storage object
  17160. // - CreateFrom() will expect Definition.UserName/Password to store the
  17161. // credentials which would be used by SetUser()
  17162. procedure DefinitionTo(Definition: TSynConnectionDefinition); override;
  17163. /// clear session and call the /auth service on the server to notify shutdown
  17164. // - is called by Destroy and SetUser/ClientSetUser methods, so you should
  17165. // not have usually to call this method directly
  17166. procedure SessionClose;
  17167. /// method calling the remote Server via a RESTful command
  17168. // - calls the InternalURI abstract method, which should be overridden with a
  17169. // local, piped or HTTP/1.1 provider
  17170. // - this method will add sign the url with the appropriate digital signature
  17171. // according to the current SessionUser property
  17172. // - this method will retry the connection in case of authentication failure
  17173. // (i.e. if the session was closed by the remote server, for any reason -
  17174. // mostly a time out) if the OnAuthentificationFailed event handler is set
  17175. function URI(const url, method: RawUTF8; Resp: PRawUTF8=nil;
  17176. Head: PRawUTF8=nil; SendData: PRawUTF8=nil): Int64Rec;
  17177. /// retrieve a list of members as a TSQLTable
  17178. // - implements REST GET collection
  17179. // - URI is 'ModelRoot/TableName' with GET method
  17180. // - SQLSelect and SQLWhere are encoded as 'select=' and 'where=' URL parameters
  17181. // (using inlined parameters via :(...): in SQLWhere is always a good idea)
  17182. // - server must return Status 200/HTML_SUCCESS OK on success
  17183. function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8 = 'RowID';
  17184. const SQLWhere: RawUTF8 = ''): TSQLTableJSON; override;
  17185. /// unlock the corresponding record
  17186. // - URI is 'ModelRoot/TableName/TableID' with UNLOCK method
  17187. // - returns true on success
  17188. function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override;
  17189. /// Execute directly a SQL statement, expecting a list of resutls
  17190. // - URI is 'ModelRoot' with GET method, and SQL statement sent as UTF-8
  17191. // - return a result table on success, nil on failure
  17192. function ExecuteList(const Tables: array of TSQLRecordClass;
  17193. const SQL: RawUTF8): TSQLTableJSON; override;
  17194. /// ask the server for its current internal state revision counter
  17195. // - this counter is incremented every time the database is modified
  17196. // - the returned value is 0 if the database doesn't support this feature
  17197. // - TSQLTable does compare this value with its internal one to check if
  17198. // its content must be updated
  17199. function ServerInternalState: cardinal;
  17200. /// check if the data may have changed of the server for this objects, and
  17201. // update it if possible
  17202. // - only working types are TSQLTableJSON and TSQLRecord descendants
  17203. // - make use of the InternalState function to check the data content revision
  17204. // - return true if Data is updated successfully, or false on any error
  17205. // during data retrieval from server (e.g. if the TSQLRecord has been deleted)
  17206. // - if Data contains only one TSQLTableJSON, PCurrentRow can point to the
  17207. // current selected row of this table, in order to refresh its value
  17208. // - use this method to refresh the client UI, e.g. via a timer
  17209. function UpdateFromServer(const Data: array of TObject; out Refreshed: boolean;
  17210. PCurrentRow: PInteger=nil): boolean; virtual;
  17211. /// send a flush command to the remote Server cache
  17212. // - this method will remotely call the Cache.Flush() methods of the server
  17213. // instance, to force cohesion of the data
  17214. // - ServerCacheFlush() with no parameter will flush all stored JSON content
  17215. // - ServerCacheFlush(aTable) will flush the cache for a given table
  17216. // - ServerCacheFlush(aTable,aID) will flush the cache for a given record
  17217. function ServerCacheFlush(aTable: TSQLRecordClass=nil; aID: TID=0): boolean; virtual;
  17218. /// you can call this method to call the remote URI root/TimeStamp
  17219. // - this can be an handy way of testing the connection, since this method
  17220. // is always available, even without authentication
  17221. // - returns TRUE if the client time correction has been retrieved
  17222. // - returns FALSE on any connection error - check LastErrorMessage and
  17223. // LastErrorException to find out the exact connection error
  17224. function ServerTimeStampSynchronize: boolean;
  17225. /// asynchronous call a 'RemoteLog' remote logging method on the server
  17226. // - as implemented by mORMot's LogView tool in server mode
  17227. // - to be used via ServerRemoteLogStart/ServerRemoteLogStop methods
  17228. // - a dedicated background thread will run the transmission process without
  17229. // blocking the main program execution, gathering log rows in chunks in case
  17230. // of high activity
  17231. // - map TOnTextWriterEcho signature, so that you would be able to set e.g.:
  17232. // ! TSQLLog.Family.EchoCustom := aClient.ServerRemoteLog;
  17233. function ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo;
  17234. const Text: RawUTF8): boolean; overload; virtual;
  17235. /// internal method able to emulate a call to TSynLog.Add.Log()
  17236. // - will compute timestamp and event text, than call the overloaded
  17237. // ServerRemoteLog() method
  17238. function ServerRemoteLog(Level: TSynLogInfo; FormatMsg: PUTF8Char;
  17239. const Args: array of const): boolean; overload;
  17240. /// start to send all logs to the server 'RemoteLog' method-based service
  17241. // - will associate the EchoCustom callback of the running log class to the
  17242. // ServerRemoteLog() method
  17243. // - if aClientOwnedByFamily is TRUE, this TSQLRestClientURI instance
  17244. // lifetime will be managed by TSynLogFamily - which is mostly wished
  17245. // - if aClientOwnedByFamily is FALSE, you should manage this instance
  17246. // life time, and may call ServerRemoteLogStop to stop remote logging
  17247. // - warning: current implementation will disable all logging for this
  17248. // TSQLRestClientURI instance, to avoid any potential concern (e.g. for
  17249. // multi-threaded process, or in case of communication error): you should
  17250. // therefore use this TSQLRestClientURI connection only for the remote log
  17251. // server, e.g. via TSQLHttpClientGeneric.CreateForRemoteLogging() - do
  17252. // not call ServerRemoteLogStart() from a high-level business client!
  17253. procedure ServerRemoteLogStart(aLogClass: TSynLogClass;
  17254. aClientOwnedByFamily: boolean);
  17255. /// stop sending all logs to the server 'RemoteLog' method-based service
  17256. // - do nothing if aClientOwnedByFamily was TRUE for ServerRemoteLogStart
  17257. procedure ServerRemoteLogStop;
  17258. /// begin a transaction
  17259. // - implements REST BEGIN collection
  17260. // - in aClient-Server environment with multiple Clients connected at the
  17261. // same time, you should better use BATCH process, specifying a positive
  17262. // AutomaticTransactionPerRow parameter to BatchStart()
  17263. // - may be used to speed up some SQL statements as Add/Update/Delete methods
  17264. // - must be ended with Commit on success
  17265. // - in the current implementation, the aTable parameter is not used yet
  17266. // - must be aborted with Rollback if any SQL statement failed
  17267. // - return true if no transaction is active, false otherwise
  17268. // !if Client.TransactionBegin(TSQLRecordPeopleObject) then
  17269. // !try
  17270. // ! // .... modify the database content, raise exceptions on error
  17271. // ! Client.Commit;
  17272. // !except
  17273. // ! Client.RollBack; // in case of error
  17274. // !end;
  17275. // - you may use the dedicated TransactionBeginRetry() method in case of
  17276. // potential Client concurrent access
  17277. function TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal=1): boolean; override;
  17278. /// begin a transaction
  17279. // - implements REST BEGIN collection
  17280. // - in aClient-Server environment with multiple Clients connected at the
  17281. // same time, you should better use BATCH process, specifying a positive
  17282. // AutomaticTransactionPerRow parameter to BatchStart()
  17283. // - this version retries a TranslationBegin() to be successfull within
  17284. // a supplied number of times
  17285. // - will retry every 100 ms for "Retries" times (excluding the connection
  17286. // time in this 100 ms time period
  17287. // - default is to retry 10 times, i.e. within 2 second timeout
  17288. // - in the current implementation, the aTable parameter is not used yet
  17289. // - typical usage should be for instance:
  17290. // !if Client.TransactionBeginRetry(TSQLRecordPeopleObject,20) then
  17291. // !try
  17292. // ! // .... modify the database content, raise exceptions on error
  17293. // ! Client.Commit;
  17294. // !except
  17295. // ! Client.RollBack; // in case of error
  17296. // !end;
  17297. function TransactionBeginRetry(aTable: TSQLRecordClass; Retries: integer=10): boolean;
  17298. /// end a transaction
  17299. // - implements REST END collection
  17300. // - write all pending SQL statements to the disk }
  17301. procedure Commit(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED;
  17302. RaiseException: boolean=false); override;
  17303. /// abort a transaction
  17304. // - implements REST ABORT collection
  17305. // - restore the previous state of the database, before the call to TransactionBegin }
  17306. procedure RollBack(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED); override;
  17307. /// begin a BATCH sequence to speed up huge database change for a given table
  17308. // - is a wrapper around TSQLRestBatch.Create() which will be stored in this
  17309. // TSQLRestClientURI instance - be aware that this won't be thread-safe
  17310. // - if you need a thread-safe "Unit Of Work" process, please use a private
  17311. // TSQLRestBatch instance and the overloaded TSQLRest.BatchSend() method
  17312. // - call BatchStartAny() or set the aTable parameter to nil if you want to
  17313. // use any kind of TSQLRecord objects within the process, not a single one
  17314. function BatchStart(aTable: TSQLRecordClass;
  17315. AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]): boolean; virtual;
  17316. /// begin a BATCH sequence to speed up huge database change for any table
  17317. // - will call the BatchStart() method with aTable = nil so that you may be
  17318. // able to use any kind of TSQLRecord class within the process
  17319. // - is a wrapper around TSQLRestBatch.Create() which will be stored in this
  17320. // TSQLRestClientURI instance - be aware that this won't be thread-safe
  17321. function BatchStartAny(AutomaticTransactionPerRow: cardinal;
  17322. Options: TSQLRestBatchOptions=[]): boolean;
  17323. /// create a new member in current BATCH sequence
  17324. // - is a wrapper around TSQLRestBatch.Add() which will be stored in this
  17325. // TSQLRestClientURI instance - be aware that this won't be thread safe
  17326. function BatchAdd(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false;
  17327. const CustomFields: TSQLFieldBits=[]): integer;
  17328. /// update a member in current BATCH sequence
  17329. // - is a wrapper around TSQLRestBatch.Update() which will be stored in this
  17330. // TSQLRestClientURI instance - be aware that this won't be thread safe
  17331. // - this method will call BeforeUpdateEvent before TSQLRestBatch.Update
  17332. function BatchUpdate(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[];
  17333. DoNotAutoComputeFields: boolean=false): integer;
  17334. /// delete a member in current BATCH sequence
  17335. // - is a wrapper around TSQLRestBatch.Update() which will be stored in this
  17336. // TSQLRestClientURI instance - be aware that this won't be thread safe
  17337. function BatchDelete(ID: TID): integer; overload;
  17338. /// delete a member in current BATCH sequence
  17339. // - is a wrapper around TSQLRestBatch.Update() which will be stored in this
  17340. // TSQLRestClientURI instance - be aware that this won't be thread safe
  17341. function BatchDelete(Table: TSQLRecordClass; ID: TID): integer; overload;
  17342. /// retrieve the current number of pending transactions in the BATCH sequence
  17343. // - every call to BatchAdd/Update/Delete methods increases this count
  17344. function BatchCount: integer;
  17345. /// execute a BATCH sequence started by BatchStart method
  17346. // - send all pending BatchAdd/Update/Delete statements to the remote server
  17347. // - URI is 'ModelRoot/TableName/0' with POST (or PUT) method
  17348. // - will return the URI Status value, i.e. 200/HTML_SUCCESS OK on success
  17349. // - a dynamic array of integers will be created in Results,
  17350. // containing all ROWDID created for each BatchAdd call, 200 (=HTML_SUCCESS)
  17351. // for all successfull BatchUpdate/BatchDelete, or 0 on error
  17352. // - any error during server-side process MUST be checked against Results[]
  17353. // (the main URI Status is 200 if about communication success, and won't
  17354. // imply that all statements in the BATCH sequence were successfull
  17355. function BatchSend(var Results: TIDDynArray): integer; overload;
  17356. /// abort a BATCH sequence started by BatchStart method
  17357. // - in short, nothing is sent to the remote server, and current BATCH
  17358. // sequence is closed
  17359. // - will Free the TSQLRestBatch stored in this TSQLRestClientURI instance
  17360. procedure BatchAbort;
  17361. /// wrapper to the protected URI method to call a method on the server, using
  17362. // a ModelRoot/[TableName/[ID/]]MethodName RESTful GET request
  17363. // - returns the HTTP error code (e.g. 200/HTML_SUCCESS on success)
  17364. // - this version will use a GET with supplied parameters (which will be encoded
  17365. // with the URL)
  17366. function CallBackGet(const aMethodName: RawUTF8;
  17367. const aNameValueParameters: array of const;
  17368. out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
  17369. aResponseHead: PRawUTF8=nil): integer;
  17370. /// wrapper to the protected URI method to call a method on the server, using
  17371. // a ModelRoot/[TableName/[ID/]]MethodName RESTful GET request
  17372. // - returns the UTF-8 decoded JSON result (server must reply with one
  17373. // "result":"value" JSON object)
  17374. // - this version will use a GET with supplied parameters (which will be encoded
  17375. // with the URL)
  17376. function CallBackGetResult(const aMethodName: RawUTF8;
  17377. const aNameValueParameters: array of const;
  17378. aTable: TSQLRecordClass=nil; aID: TID=0): RawUTF8;
  17379. /// wrapper to the protected URI method to call a method on the server, using
  17380. // a ModelRoot/[TableName/[ID/]]MethodName RESTful PUT request
  17381. // - returns the HTTP error code (e.g. 200/HTML_SUCCESS on success)
  17382. // - this version will use a PUT with the supplied raw UTF-8 data
  17383. function CallBackPut(const aMethodName, aSentData: RawUTF8;
  17384. out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
  17385. aResponseHead: PRawUTF8=nil): integer;
  17386. /// wrapper to the protected URI method to call a method on the server, using
  17387. // a ModelRoot/[TableName/[ID/]]MethodName RESTful with any kind of request
  17388. // - returns the HTTP error code (e.g. 200/HTML_SUCCESS on success)
  17389. // - for GET/PUT methods, you should better use CallBackGet/CallBackPut
  17390. function CallBack(method: TSQLURIMethod; const aMethodName,aSentData: RawUTF8;
  17391. out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
  17392. aResponseHead: PRawUTF8=nil): integer;
  17393. /// register one or several Services on the client side via their interfaces
  17394. // - this methods expects a list of interfaces to be registered to the client
  17395. // (e.g. [TypeInfo(IMyInterface)])
  17396. // - instance implementation pattern will be set by the appropriate parameter
  17397. // - will return true on success, false if registration failed (e.g. if any of
  17398. // the supplied interfaces is not correct or is not available on the server)
  17399. // - that is, server side will be called to check for the availability of
  17400. // each interface
  17401. // - you can specify an optional custom contract for the first interface
  17402. function ServiceRegister(const aInterfaces: array of PTypeInfo;
  17403. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  17404. const aContractExpected: RawUTF8=''): boolean; overload; virtual;
  17405. /// register a Service on the client side via its interface
  17406. // - this methods expects one interface to be registered to the client, as
  17407. // ! Client.ServiceRegister(TypeInfo(IMyInterface),sicShared);
  17408. // - instance implementation pattern will be set by the appropriate parameter
  17409. // - will return the corresponding fake class factory on success, nil if
  17410. // registration failed (e.g. if any of supplied interfaces is not correct or
  17411. // is not available on the server)
  17412. // - that is, server side will be called to check for the availability of
  17413. // each interface
  17414. // - you can specify an optional custom contract for the first interface
  17415. function ServiceRegister(aInterface: PTypeInfo;
  17416. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  17417. const aContractExpected: RawUTF8=''): TServiceFactory; overload;
  17418. /// register and retrieve the sicClientDriven Service instance
  17419. // - will return TRUE on success, filling Obj output variable with the
  17420. // corresponding interface instance
  17421. // - will return FALSE on error
  17422. function ServiceRegisterClientDriven(aInterface: PTypeInfo; out Obj;
  17423. const aContractExpected: RawUTF8=''): boolean; overload;
  17424. /// register one or several Services on the client side via their interfaces
  17425. // - this method expects the interface(s) to have been registered previously:
  17426. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]);
  17427. function ServiceDefine(const aInterfaces: array of TGUID;
  17428. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  17429. const aContractExpected: RawUTF8=''): boolean; overload;
  17430. /// register a Service on the client side via its interface
  17431. // - this method expects the interface(s) to have been registered previously:
  17432. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]);
  17433. function ServiceDefine(const aInterface: TGUID;
  17434. aInstanceCreation: TServiceInstanceImplementation=sicSingle;
  17435. const aContractExpected: RawUTF8=''): TServiceFactoryClient; overload;
  17436. /// register and retrieve the sicClientDriven Service instance
  17437. // - this method expects the interface(s) to have been registered previously:
  17438. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]);
  17439. function ServiceDefineClientDriven(const aInterface: TGUID; out Obj;
  17440. const aContractExpected: RawUTF8=''): boolean; overload;
  17441. /// allow to notify a server the services this client may be actually capable
  17442. // - when this client will connect to a remote server to access its services,
  17443. // it will register its own services, supplying its TSQLRestServer instance,
  17444. // and its corresponding public URI, within its '_contract_' internal call
  17445. // - it will allow automatic service discovery of Peer To Peer Servers,
  17446. // without the need of an actual centralized SOA catalog service: any
  17447. // client could retrieve an associated REST server for a given service,
  17448. // via the ServiceRetrieveAssociated method
  17449. procedure ServicePublishOwnInterfaces(OwnServer: TSQLRestServer);
  17450. /// return all REST server URI associated to this client, for a given
  17451. // service name, the latest registered in first position
  17452. // - will lookup for the Interface name without the initial 'I', e.g.
  17453. // 'Calculator' for ICalculator - warning: research is case-sensitive
  17454. // - this methods is the reverse from ServicePublishOwnInterfaces: it allows
  17455. // to guess an associated REST server which may implement a given service
  17456. function ServiceRetrieveAssociated(const aServiceName: RawUTF8;
  17457. out URI: TSQLRestServerURIDynArray): boolean; overload;
  17458. /// return all REST server URI associated to this client, for a given service
  17459. // - here the service is specified as its TGUID, e.g. IMyInterface
  17460. // - this method expects the interface to have been registered previously:
  17461. // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]);
  17462. // - the URI[] output array contains the matching server URIs, the latest
  17463. // registered in first position
  17464. // - this methods is the reverse from ServicePublishOwnInterfaces: it allows
  17465. // to guess an associated REST server which may implement a given service
  17466. function ServiceRetrieveAssociated(const aInterface: TGUID;
  17467. out URI: TSQLRestServerURIDynArray): boolean; overload;
  17468. {$ifdef MSWINDOWS}
  17469. /// set a HWND/WM_* pair to let interface-based services notification
  17470. // callbacks be processed safely in the main UI thread, via Windows messages
  17471. // - by default callbacks are executed in the transmission thread, e.g.
  17472. // the WebSockets client thread: using VCL Synchronize() method may
  17473. // trigger some unexpected race conditions, e.g. when asynchronous
  17474. // notifications are received during a blocking REST command - this
  17475. // message-based mechanism would allow safe and easy notification for
  17476. // any VCL client application
  17477. // - the associated ServiceNotificationMethodExecute() method shall be
  17478. // called in the client HWND TForm for the defined WM_* message
  17479. procedure ServiceNotificationMethodViaMessages(hWnd: HWND; Msg: UINT);
  17480. /// event to be triggered when a WM_* message is received from
  17481. // the internal asynchronous notification system, to run the callback
  17482. // in the main UI thread
  17483. // - WM_* message identifier should have been set e.g. via the associated
  17484. // ServiceNotificationMethodViaMessages(Form.Handle,WM_USER)
  17485. // - message would be sent for any interface-based service method callback
  17486. // which expects no result (i.e. no out parameter nor function result),
  17487. // so is safely handled as asynchronous notification
  17488. // - is defines as a class procedure, since the underlying TSQLRestClientURI
  17489. // instance has no impact here: a single WM_* handler is enough for
  17490. // several TSQLRestClientURI instances
  17491. class procedure ServiceNotificationMethodExecute(var Msg : TMessage);
  17492. {$endif MSWINDOWS}
  17493. published
  17494. /// low-level error code, as returned by server
  17495. // - check this value about HTML_* constants
  17496. // - HTML_SUCCESS or HTML_CREATED mean no error
  17497. // - otherwise, check LastErrorMessage property for additional information
  17498. // - this property value will record status codes returned by URI() method
  17499. property LastErrorCode: integer read fLastErrorCode;
  17500. /// low-level error message, as returned by server
  17501. // - this property value will record content returned by URI() method in
  17502. // case of an error, or '' if LastErrorCode is HTML_SUCCESS or HTML_CREATED
  17503. property LastErrorMessage: RawUTF8 read fLastErrorMessage;
  17504. /// low-level exception class, if any
  17505. // - will record any Exception class raised within URI() method
  17506. // - contains nil if URI() execution did not raise any exception (which
  17507. // is the most expected behavior, since server-side errors are trapped
  17508. // into LastErrorCode/LastErrorMessage properties
  17509. property LastErrorException: ExceptClass read fLastErrorException;
  17510. /// maximum additional retry occurence
  17511. // - defaut is 0, i.e. will retry once
  17512. // - set OnAuthentificationFailed to nil in order to avoid any retry
  17513. property MaximumAuthentificationRetry: Integer
  17514. read fMaximumAuthentificationRetry write fMaximumAuthentificationRetry;
  17515. /// if the client shall retry once in case of "408 REQUEST TIMEOUT" error
  17516. property RetryOnceOnTimeout: Boolean
  17517. read fRetryOnceOnTimeout write fRetryOnceOnTimeout;
  17518. /// the current session ID as set after a successfull SetUser() method call
  17519. // - equals 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED) if the session
  17520. // is not started yet - i.e. if SetUser() call failed
  17521. // - equals 1 (CONST_AUTHENTICATION_NOT_USED) if authentication mode
  17522. // is not enabled - i.e. after a fresh Create() without SetUser() call
  17523. property SessionID: cardinal read fSessionID;
  17524. /// the remote server executable name, as retrieved after a SetUser() success
  17525. property SessionServer: RawUTF8 read fSessionServer;
  17526. /// the remote server version, as retrieved after a SetUser() success
  17527. property SessionVersion: RawUTF8 read fSessionVersion;
  17528. public
  17529. /// the current user as set by SetUser() method
  17530. // - contans nil if no User is currently authenticated
  17531. // - once authenticated, a TSQLAuthUser instance is set, with its ID,
  17532. // LogonName, DisplayName, PasswordHashHexa and GroupRights (filled with a
  17533. // TSQLAuthGroup ID casted as a pointer) properties - you can retrieve any
  17534. // optional binary data associated with this user via RetrieveBlobFields()
  17535. property SessionUser: TSQLAuthUser read fSessionUser;
  17536. {$ifndef LVCL}
  17537. /// set a callback event to be executed in loop during remote blocking
  17538. // process, e.g. to refresh the UI during a somewhat long request
  17539. // - if not set, the request will be executed in the current thread,
  17540. // so may block the User Interface
  17541. // - you can assign a callback to this property, calling for instance
  17542. // Application.ProcessMessages, to execute the remote request in a
  17543. // background thread, but let the UI still be reactive: the
  17544. // TLoginForm.OnIdleProcess and OnIdleProcessForm methods of
  17545. // mORMotUILogin.pas will match this property expectations
  17546. property OnIdle: TOnIdleSynBackgroundThread read fOnIdle write fOnIdle;
  17547. /// TRUE if the background thread is active, and OnIdle event is called
  17548. // during process
  17549. // - to be used e.g. to ensure no re-entrance from User Interface messages
  17550. property OnIdleBackgroundThreadActive: Boolean read GetOnIdleBackgroundThreadActive;
  17551. {$endif}
  17552. /// this Event is called in case of remote authentication failure
  17553. // - client software can ask the user to enter a password and user name
  17554. // - if no event is specified, the URI() method will return directly
  17555. // an HTML_FORBIDDEN "403 Forbidden" error code
  17556. property OnAuthentificationFailed: TOnAuthentificationFailed
  17557. read fOnAuthentificationFailed write fOnAuthentificationFailed;
  17558. /// this Event is called if URI() was not successfull
  17559. // - the callback would have all needed information
  17560. property OnFailed: TOnClientFailed read fOnFailed write fOnFailed;
  17561. /// this Event is called when a user is authenticated
  17562. // - is called always, on each TSQLRestClientURI.SetUser call
  17563. // - you can check the SessionUser property to retrieve the current
  17564. // authenticated user, or nil if authentication failed
  17565. // - could be used to refresh the User Interface layout according to
  17566. // current authenticated user rights
  17567. property OnSetUser: TNotifyEvent read fOnSetUser write fOnSetUser;
  17568. end;
  17569. /// Rest client with remote access to a server through a dll
  17570. // - use only one TURIMapRequest function for the whole communication
  17571. // - the data is stored in Global system memory, and freed by GlobalFree()
  17572. TSQLRestClientURIDll = class(TSQLRestClientURI)
  17573. private
  17574. /// used by Create(from dll) constructor
  17575. fLibraryHandle: cardinal;
  17576. protected
  17577. Func: TURIMapRequest;
  17578. /// method calling the RESTful server through a DLL or executable, using
  17579. // direct memory
  17580. procedure InternalURI(var Call: TSQLRestURIParams); override;
  17581. /// overridden protected method do nothing (direct DLL access has no connection)
  17582. function InternalCheckOpen: boolean; override;
  17583. /// overridden protected method do nothing (direct DLL access has no connection)
  17584. procedure InternalClose; override;
  17585. public
  17586. /// connect to a server from a remote function
  17587. constructor Create(aModel: TSQLModel; aRequest: TURIMapRequest); reintroduce; overload;
  17588. /// connect to a server contained in a shared library
  17589. // - this dll must contain at least a URIRequest entry
  17590. // - raise an exception if the shared library is not found or invalid
  17591. constructor Create(aModel: TSQLModel; const DllName: TFileName); reintroduce; overload;
  17592. /// release memory and handles
  17593. destructor Destroy; override;
  17594. end;
  17595. /// Rest client with redirection to another TSQLRest instance
  17596. TSQLRestClientRedirect = class(TSQLRestClientURI)
  17597. protected
  17598. fRedirectedServer: TSQLRestServer;
  17599. fRedirectedClient: TSQLRestClientURI;
  17600. /// method calling the associated RESTful instance
  17601. procedure InternalURI(var Call: TSQLRestURIParams); override;
  17602. /// overridden protected method which returns TRUE if redirection is enabled
  17603. function InternalCheckOpen: boolean; override;
  17604. /// this overridden protected method does nothing
  17605. procedure InternalClose; override;
  17606. public
  17607. /// prepare the redirection, to be enabled later via RedirectTo()
  17608. // - the supplied aModel instance would be owned by this class
  17609. constructor Create(aModel: TSQLModel); overload; override;
  17610. /// would pass all client commands to the supplied TSQLRest instance
  17611. // - aRedirected is expected to be either a TSQLRestClientURI or
  17612. // a TSQLRestServer
  17613. // - will make a copy of the aRedirected.Model, and own it
  17614. constructor Create(aRedirected: TSQLRest); reintroduce; overload;
  17615. /// would pass all client commands to the supplied TSQLRestServer instance
  17616. // - aRedirected would be owned by this TSQLRestClientRedirect
  17617. constructor CreateOwned(aRedirected: TSQLRestServer); reintroduce;
  17618. /// allows to change redirection to a client on the fly
  17619. // - if aRedirected is nil, redirection would be disabled and any URI() call
  17620. // would return an HTML_GATEWAYTIMEOUT 504 error status
  17621. procedure RedirectTo(aRedirected: TSQLRest);
  17622. end;
  17623. {$ifdef MSWINDOWS}
  17624. /// Rest client with remote access to a server through Windows messages
  17625. // - use only one TURIMapRequest function for the whole communication
  17626. // - the data is sent and received by using the standard and fast WM_COPYDATA message
  17627. // - named pipes seems to be somewhat better for bigger messages under XP
  17628. // - this class is thread-safe, since its URI() method is protected by a lock
  17629. TSQLRestClientURIMessage = class(TSQLRestClientURI)
  17630. protected
  17631. /// the HWND of the server process, retrieved by InternalCheckOpen() method
  17632. fServerWindow: HWND;
  17633. /// the Window name used of the server process
  17634. fServerWindowName: string;
  17635. /// the HWND of the client process, as set by Create() method
  17636. fClientWindow: HWND;
  17637. /// the Window name used, if created internaly
  17638. fClientWindowName: string;
  17639. /// the time out to be used, in mili seconds
  17640. fTimeOutMS: cardinal;
  17641. /// if InternalURI will process the Windows Messages loop
  17642. fDoNotProcessMessages: boolean;
  17643. /// the expected current response
  17644. // - this value is set from the incoming WM_COPYDATA
  17645. // - this value is set to #0 (i.e. string of one #0 char) while waiting
  17646. // for a WM_COPYDATA message in URI() method
  17647. fCurrentResponse: RawUTF8;
  17648. constructor RegisteredClassCreateFrom(aModel: TSQLModel;
  17649. aDefinition: TSynConnectionDefinition); override;
  17650. /// method calling the RESTful server by using Windows WM_COPYDATA messages
  17651. procedure InternalURI(var Call: TSQLRestURIParams); override;
  17652. /// overridden protected method to handle Windows Message loop connection
  17653. function InternalCheckOpen: boolean; override;
  17654. /// overridden protected method to close Windows Message
  17655. procedure InternalClose; override;
  17656. public
  17657. /// connect to a server from its window name
  17658. // - ServerWindowName is of UnicodeString type since Delphi 2009
  17659. // (direct use of FindWindow()=FindWindowW() Win32 API)
  17660. // - this version must supply a Client Window handle
  17661. constructor Create(aModel: TSQLModel; const ServerWindowName: string;
  17662. ClientWindow: HWND; TimeOutMS: cardinal); reintroduce; overload;
  17663. /// connect to a server from its window name
  17664. // - ServerWindowName is of UnicodeString type since Delphi 2009
  17665. // (direct use of FindWindow()=FindWindowW() Win32 API)
  17666. // - this version will instanciante and create a Client Window from
  17667. // a Window Name, by using low level Win32 API: therefore, the Forms unit
  17668. // is not needed with this constructor (save some KB)
  17669. constructor Create(aModel: TSQLModel; const ServerWindowName,
  17670. ClientWindowName: string; TimeOutMS: cardinal); reintroduce; overload;
  17671. /// release the internal Window class created, if any
  17672. destructor Destroy; override;
  17673. /// save the TSQLRestClientURIMessage properties into a persistent storage object
  17674. // - CreateFrom() will expect Definition.ServerName to store the
  17675. // ServerWindowName, and Definition.DatabaseName to be the ClientWindowName
  17676. procedure DefinitionTo(Definition: TSynConnectionDefinition); override;
  17677. /// event to be triggered when a WM_COPYDATA message is received from the server
  17678. // - to be called by the corresponding "message WM_COPYDATA;" method in the
  17679. // client TForm instance
  17680. procedure WMCopyData(var Msg : TWMCopyData); message WM_COPYDATA;
  17681. /// define if the client will process the Windows Messages loop
  17682. // - set to TRUE if the client is used outside the main GUI application thread
  17683. property DoNotProcessMessages: boolean read fDoNotProcessMessages write fDoNotProcessMessages;
  17684. end;
  17685. /// Rest client with remote access to a server through a Named Pipe
  17686. // - named pipe is fast and optimized under Windows
  17687. // - can be accessed localy or remotely
  17688. // - this class is thread-safe, since its URI() method is protected by a lock
  17689. TSQLRestClientURINamedPipe = class(TSQLRestClientURI)
  17690. private
  17691. /// handle for '\\.\pipe\mORMot_TEST' e.g.
  17692. fServerPipe: THandle;
  17693. /// the pipe name
  17694. fPipeName: TFileName;
  17695. {$ifndef ANONYMOUSNAMEDPIPE}
  17696. {$ifndef NOSECURITYFORNAMEDPIPECLIENTS}
  17697. fPipeSecurityAttributes: TSecurityAttributes;
  17698. fPipeSecurityDescriptor: array[0..SECURITY_DESCRIPTOR_MIN_LENGTH] of byte;
  17699. {$endif}
  17700. {$endif}
  17701. protected
  17702. constructor RegisteredClassCreateFrom(aModel: TSQLModel;
  17703. aDefinition: TSynConnectionDefinition); override;
  17704. /// method calling the RESTful server through a DLL or executable, by using
  17705. // a named pipe (faster than TCP/IP or HTTP connection)
  17706. // - return status code in result.Lo
  17707. // - return database internal state in result.Hi
  17708. // - status code 501 HTML_NOTIMPLEMENTED if no server is available
  17709. procedure InternalURI(var Call: TSQLRestURIParams); override;
  17710. /// overridden protected method to handle named-pipe connection
  17711. function InternalCheckOpen: boolean; override;
  17712. /// overridden protected method to close named-pipe connection
  17713. procedure InternalClose; override;
  17714. public
  17715. /// connect to a server contained in a running application
  17716. // - the server must have been declared by a previous
  17717. // TSQLRestServer.ExportServer(ApplicationName) call
  17718. // with ApplicationName as user-defined server identifier ('DBSERVER' e.g.)
  17719. // - ApplicationName is of UnicodeString type since Delphi 2009
  17720. // (direct use of Wide Win32 API version)
  17721. // - this server identifier is appended to '\\.\pipe\mORMot_' to obtain
  17722. // the full pipe name to connect to ('\\.\pipe\mORMot__DBSERVER' e.g.)
  17723. // - this server identifier may also contain a remote computer name, and
  17724. // must be fully qualified ('\\ServerName\pipe\ApplicationName' e.g.)
  17725. // - raise an exception if the server is not running or invalid
  17726. constructor Create(aModel: TSQLModel; const ApplicationName: TFileName); reintroduce;
  17727. /// save the TSQLRestClientURIMessage properties into a persistent storage object
  17728. // - CreateFrom() will expect Definition.ServerName to store the
  17729. // expected ApplicationName
  17730. procedure DefinitionTo(Definition: TSynConnectionDefinition); override;
  17731. end;
  17732. {$endif Win32}
  17733. /// will define a validation to be applied to a TSQLRecord field, using
  17734. // if necessary an associated TSQLRest instance and a TSQLRecord class
  17735. // - a typical usage is to validate a value to be unique in the table
  17736. // (implemented in the TSynValidateUniqueField class)
  17737. // - the optional associated parameters are to be supplied JSON-encoded
  17738. // - ProcessRest and ProcessRec properties will be filled before Process
  17739. // method call by TSQLRecord.Validate()
  17740. TSynValidateRest = class(TSynValidate)
  17741. protected
  17742. fProcessRest: TSQLRest;
  17743. fProcessRec: TSQLRecord;
  17744. public
  17745. /// the associated TSQLRest instance
  17746. // - this value is updated by TSQLRecord.Validate with the current
  17747. // TSQLRest used for the validation
  17748. // - it can be used in the overridden Process method
  17749. property ProcessRest: TSQLRest read fProcessRest;
  17750. /// the associated TSQLRecord instance
  17751. // - this value is updated by TSQLRecord.Validate with the current
  17752. // TSQLRecord instance to be validated
  17753. // - it can be used in the overridden Process method
  17754. property ProcessRec: TSQLRecord read fProcessRec;
  17755. end;
  17756. /// will define a validation for a TSQLRecord Unique text field
  17757. // - this class will handle only textual fields, not numeric values
  17758. // - it will check that the field value is not void
  17759. // - it will check that the field value is not a duplicate
  17760. TSynValidateUniqueField = class(TSynValidateRest)
  17761. public
  17762. /// perform the unique field validation action to the specified value
  17763. // - duplication value check will use ProcessRest and ProcessRec properties,
  17764. // as set by TSQLRecord.Validate
  17765. function Process(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string): boolean; override;
  17766. end;
  17767. /// will define an unicity validation for a set of TSQLRecord text fields
  17768. // - field names should be specified as CSV in the JSON "FieldNames" property
  17769. // in the constructor, or the Parameters field, e.g. like
  17770. // ! TSQLSampleRecord.AddFilterOrValidate('propA',
  17771. // ! TSynValidateUniqueFields.Create('{"FieldNames":"propA,propB"}'));
  17772. // - this class will handle only textual fields, not numeric values
  17773. // - it will check that the field values are not a duplicate
  17774. TSynValidateUniqueFields = class(TSynValidateRest)
  17775. protected
  17776. fFieldNames: TRawUTF8DynArray;
  17777. procedure SetParameters(const Value: RawUTF8); override;
  17778. public
  17779. /// perform the unique fields validation action to the specified value
  17780. // - duplication value check will use ProcessRest and ProcessRec properties,
  17781. // as set by TSQLRecord.Validate
  17782. function Process(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string): boolean; override;
  17783. /// the validated field names
  17784. property FieldNames: TRawUTF8DynArray read fFieldNames;
  17785. end;
  17786. /// a WHERE constraint as set by the TSQLVirtualTable.Prepare() method
  17787. TSQLVirtualTablePreparedConstraint = packed record
  17788. /// Column on left-hand side of constraint
  17789. // - The first column of the virtual table is column 0
  17790. // - The ROWID of the virtual table is column -1
  17791. // - Hidden columns are counted when determining the column index
  17792. // - if this field contains VIRTUAL_TABLE_IGNORE_COLUMN (-2), TSQLVirtualTable.
  17793. // Prepare() should ignore this entry
  17794. Column: integer;
  17795. /// The associated expression
  17796. // - TSQLVirtualTable.Prepare() must set Value.VType to not svtUnknown
  17797. // (e.g. to svtNull), if an expression is expected at vt_BestIndex() call
  17798. // - TSQLVirtualTableCursor.Search() will receive an expression value,
  17799. // to be retrieved e.g. via sqlite3_value_*() functions
  17800. Value: TSQLVar;
  17801. /// Constraint operator
  17802. // - MATCH keyword is parsed into soBeginWith, and should be handled as
  17803. // soBeginWith, soContains or soSoundsLike* according to the effective
  17804. // expression text value ('text*', '%text'...)
  17805. Operation: TCompareOperator;
  17806. /// If true, the constraint is assumed to be fully handled
  17807. // by the virtual table and is not checked again by SQLite
  17808. // - By default (OmitCheck=false), the SQLite core double checks all
  17809. // constraints on each row of the virtual table that it receives
  17810. // - TSQLVirtualTable.Prepare() can set this property to true
  17811. OmitCheck: boolean;
  17812. end;
  17813. PSQLVirtualTablePreparedConstraint = ^TSQLVirtualTablePreparedConstraint;
  17814. /// an ORDER BY clause as set by the TSQLVirtualTable.Prepare() method
  17815. // - warning: this structure should match exactly TSQLite3IndexOrderBy as
  17816. // defined in SynSQLite3
  17817. TSQLVirtualTablePreparedOrderBy = record
  17818. /// Column number
  17819. // - The first column of the virtual table is column 0
  17820. // - The ROWID of the virtual table is column -1
  17821. // - Hidden columns are counted when determining the column index.
  17822. Column: Integer;
  17823. /// True for DESCending order, false for ASCending order.
  17824. Desc: boolean;
  17825. end;
  17826. /// abstract planning execution of a query, as set by TSQLVirtualTable.Prepare
  17827. TSQLVirtualTablePreparedCost = (
  17828. costFullScan, costScanWhere, costSecondaryIndex, costPrimaryIndex);
  17829. /// the WHERE and ORDER BY statements as set by TSQLVirtualTable.Prepare
  17830. // - Where[] and OrderBy[] are fixed sized arrays, for fast and easy code
  17831. TSQLVirtualTablePrepared = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  17832. public
  17833. /// number of WHERE statement parameters in Where[] array
  17834. WhereCount: integer;
  17835. /// numver of ORDER BY statement parameters in OrderBy[]
  17836. OrderByCount: integer;
  17837. /// if true, the ORDER BY statement is assumed to be fully handled
  17838. // by the virtual table and is not checked again by SQLite
  17839. // - By default (OmitOrderBy=false), the SQLite core sort all rows of the
  17840. // virtual table that it receives according in order
  17841. OmitOrderBy: boolean;
  17842. /// Estimated cost of using this prepared index
  17843. // - SQLite uses this value to make a choice between several calls to
  17844. // the TSQLVirtualTable.Prepare() method with several expressions
  17845. EstimatedCost: TSQLVirtualTablePreparedCost;
  17846. /// Estimated number of rows of using this prepared index
  17847. // - does make sense only if EstimatedCost=costFullScan
  17848. // - SQLite uses this value to make a choice between several calls to
  17849. // the TSQLVirtualTable.Prepare() method with several expressions
  17850. // - is used only starting with SQLite 3.8.2
  17851. EstimatedRows: Int64;
  17852. /// WHERE statement parameters, in TSQLVirtualTableCursor.Search() order
  17853. Where: array[0..MAX_SQLFIELDS-1] of TSQLVirtualTablePreparedConstraint;
  17854. /// ORDER BY statement parameters
  17855. OrderBy: array[0..MAX_SQLFIELDS-1] of TSQLVirtualTablePreparedOrderBy;
  17856. /// returns TRUE if there is only one ID=? statement in this search
  17857. function IsWhereIDEquals(CalledFromPrepare: Boolean): boolean;
  17858. {$ifdef HASINLINE}inline;{$endif}
  17859. /// returns TRUE if there is only one FieldName=? statement in this search
  17860. function IsWhereOneFieldEquals: boolean;
  17861. {$ifdef HASINLINE}inline;{$endif}
  17862. end;
  17863. PSQLVirtualTablePrepared = ^TSQLVirtualTablePrepared;
  17864. TSQLVirtualTableCursor = class;
  17865. /// class-reference type (metaclass) of a cursor on an abstract Virtual Table
  17866. TSQLVirtualTableCursorClass = class of TSQLVirtualTableCursor;
  17867. /// the possible features of a Virtual Table
  17868. // - vtWrite is to be set if the table is not Read/Only
  17869. // - vtTransaction if handles vttBegin, vttSync, vttCommit, vttRollBack
  17870. // - vtSavePoint if handles vttSavePoint, vttRelease, vttRollBackTo
  17871. // - vtWhereIDPrepared if the ID=? WHERE statement will be handled in
  17872. // TSQLVirtualTableCursor.Search()
  17873. TSQLVirtualTableFeature = (vtWrite, vtTransaction, vtSavePoint,
  17874. vtWhereIDPrepared);
  17875. /// a set of features of a Virtual Table
  17876. TSQLVirtualTableFeatures = set of TSQLVirtualTableFeature;
  17877. /// used to store and handle the main specifications of a TSQLVirtualTableModule
  17878. TVirtualTableModuleProperties = record
  17879. /// a set of features of a Virtual Table
  17880. Features: TSQLVirtualTableFeatures;
  17881. /// the associated cursor class
  17882. CursorClass: TSQLVirtualTableCursorClass;
  17883. /// the associated TSQLRecord class
  17884. // - used to retrieve the field structure with all collations
  17885. RecordClass: TSQLRecordClass;
  17886. /// the associated TSQLRestStorage class used for storage
  17887. // - is e.g. TSQLRestStorageInMemory for TSQLVirtualTableJSON,
  17888. // TSQLRestStorageExternal for TSQLVirtualTableExternal, or nil for
  17889. // TSQLVirtualTableLog
  17890. StaticClass: TSQLRestStorageClass;
  17891. /// can be used to customize the extension of the filename
  17892. // - the '.' is not to be included
  17893. FileExtension: TFileName;
  17894. end;
  17895. /// parent class able to define a Virtual Table module
  17896. // - in order to implement a new Virtual Table type, you'll have to define a so
  17897. // called "Module" to handle the fields and data access and an associated
  17898. // TSQLVirtualTableCursorClass for handling the SELECT statements
  17899. // - for our framework, the SQLite3 unit will inherit from this class to define
  17900. // a TSQLVirtualTableModuleSQLite3 class, which will register the associated
  17901. // virtual table definition into a SQLite3 connection, on the server side
  17902. // - children should override abstract methods in order to implement the
  17903. // association with the database engine itself
  17904. TSQLVirtualTableModule = class
  17905. protected
  17906. fModuleName: RawUTF8;
  17907. fTableClass: TSQLVirtualTableClass;
  17908. fServer: TSQLRestServer;
  17909. fFeatures: TVirtualTableModuleProperties;
  17910. fFilePath: TFileName;
  17911. public
  17912. /// create the Virtual Table instance according to the supplied class
  17913. // - inherited constructors may register the Virtual Table to the specified
  17914. // database connection
  17915. constructor Create(aTableClass: TSQLVirtualTableClass;
  17916. aServer: TSQLRestServer); virtual;
  17917. /// retrieve the file name to be used for a specific Virtual Table
  17918. // - returns by default a file located in the executable folder, with the
  17919. // table name as file name, and module name as extension
  17920. function FileName(const aTableName: RawUTF8): TFileName; virtual;
  17921. /// the Virtual Table module features
  17922. property Features: TSQLVirtualTableFeatures read fFeatures.Features;
  17923. /// the associated virtual table class
  17924. property TableClass: TSQLVirtualTableClass read fTableClass;
  17925. /// the associated virtual table cursor class
  17926. property CursorClass: TSQLVirtualTableCursorClass read fFeatures.CursorClass;
  17927. /// the associated TSQLRestStorage class used for storage
  17928. // - e.g. returns TSQLRestStorageInMemory for TSQLVirtualTableJSON,
  17929. // or TSQLRestStorageExternal for TSQLVirtualTableExternal, or
  17930. // either nil for TSQLVirtualTableLog
  17931. property StaticClass: TSQLRestStorageClass read fFeatures.StaticClass;
  17932. /// the associated TSQLRecord class
  17933. // - is mostly nil, e.g. for TSQLVirtualTableJSON
  17934. // - used to retrieve the field structure for TSQLVirtualTableLog e.g.
  17935. property RecordClass: TSQLRecordClass read fFeatures.RecordClass;
  17936. /// the extension of the filename (without any left '.')
  17937. property FileExtension: TFileName read fFeatures.FileExtension;
  17938. /// the full path to be used for the filename
  17939. // - is '' by default, i.e. will use the executable path
  17940. // - you can specify here a custom path, which will be used by the FileName
  17941. // method to retrieve the .json/.data full file
  17942. property FilePath: TFileName read fFilePath write fFilePath;
  17943. /// the associated Server instance
  17944. // - may be nil, in case of direct access to the virtual table
  17945. property Server: TSQLRestServer read fServer;
  17946. /// the corresponding module name
  17947. property ModuleName: RawUTF8 read fModuleName;
  17948. end;
  17949. /// the available transaction levels
  17950. TSQLVirtualTableTransaction = (
  17951. vttBegin, vttSync, vttCommit, vttRollBack,
  17952. vttSavePoint, vttRelease, vttRollBackTo);
  17953. /// abstract class able to access a Virtual Table content
  17954. // - override the Prepare/Structure abstract virtual methods for reading
  17955. // access to the virtual table content
  17956. // - you can optionaly override Drop/Delete/Insert/Update/Rename/Transaction
  17957. // virtual methods to allow content writing to the virtual table
  17958. // - the same virtual table mechanism can be used with several database module,
  17959. // with diverse database engines
  17960. TSQLVirtualTable = class
  17961. protected
  17962. fModule: TSQLVirtualTableModule;
  17963. fTableName: RawUTF8;
  17964. fStatic: TSQLRest;
  17965. fStaticStorage: TSQLRestStorage;
  17966. fStaticTable: TSQLRecordClass;
  17967. fStaticTableIndex: integer;
  17968. public
  17969. /// create the virtual table access instance
  17970. // - the created instance will be released when the virtual table will be
  17971. // disconnected from the DB connection (e.g. xDisconnect method for SQLite3)
  17972. // - shall raise an exception in case of invalid parameters (e.g. if the
  17973. // supplied module is not associated to a TSQLRestServer instance)
  17974. // - aTableName will be checked against the current aModule.Server.Model
  17975. // to retrieve the corresponding TSQLRecordVirtualTableAutoID class and
  17976. // create any associated Static: TSQLRestStorage instance
  17977. constructor Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8;
  17978. FieldCount: integer; Fields: PPUTF8CharArray); virtual;
  17979. /// release the associated memory, especially the Static instance
  17980. destructor Destroy; override;
  17981. /// retrieve the corresponding module name
  17982. // - will use the class name, triming any T/TSQL/TSQLVirtual/TSQLVirtualTable*
  17983. // - when the class is instanciated, it will be faster to retrieve the same
  17984. // value via Module.ModuleName
  17985. class function ModuleName: RawUTF8;
  17986. /// a generic method to get a 'CREATE TABLE' structure from a supplied
  17987. // TSQLRecord class
  17988. // - is called e.g. by the Structure method
  17989. class function StructureFromClass(aClass: TSQLRecordClass;
  17990. const aTableName: RawUTF8): RawUTF8;
  17991. /// the associated Virtual Table module
  17992. property Module: TSQLVirtualTableModule read fModule;
  17993. /// the name of the Virtual Table, as specified following the TABLE keyword
  17994. // in the CREATE VIRTUAL TABLE statement
  17995. property TableName: RawUTF8 read fTableName;
  17996. public { virtual methods to be overridden }
  17997. /// should return the main specifications of the associated TSQLVirtualTableModule
  17998. class procedure GetTableModuleProperties(
  17999. var aProperties: TVirtualTableModuleProperties); virtual; abstract;
  18000. /// called to determine the best way to access the virtual table
  18001. // - will prepare the request for TSQLVirtualTableCursor.Search()
  18002. // - in Where[], Expr must be set to not 0 if needed for Search method,
  18003. // and OmitCheck to true if double check is not necessary
  18004. // - OmitOrderBy must be set to true if double sort is not necessary
  18005. // - EstimatedCost and EstimatedRows should receive the estimated cost
  18006. // - default implementation will let the DB engine perform the search,
  18007. // and prepare for ID=? statement if vtWhereIDPrepared was set
  18008. function Prepare(var Prepared: TSQLVirtualTablePrepared): boolean; virtual;
  18009. /// should retrieve the format (the names and datatypes of the columns) of
  18010. // the virtual table, as expected by sqlite3_declare_vtab()
  18011. // - default implementation is to retrieve the structure for the associated
  18012. // Module.RecordClass property (as set by GetTableModuleProperties) or
  18013. // the Static.StoredClass: in both cases, column numbering will follow
  18014. // the TSQLRecord published field order (TSQLRecord.RecordProps.Fields[])
  18015. function Structure: RawUTF8; virtual;
  18016. /// called when a DROP TABLE statement is executed against the virtual table
  18017. // - should return true on success, false otherwise
  18018. // - does nothing by default, and returns false, i.e. always fails
  18019. function Drop: boolean; virtual;
  18020. /// called to delete a virtual table row
  18021. // - should return true on success, false otherwise
  18022. // - does nothing by default, and returns false, i.e. always fails
  18023. function Delete(aRowID: Int64): boolean; virtual;
  18024. /// called to insert a virtual table row content from an array of TSQLVar
  18025. // - should return true on success, false otherwise
  18026. // - should return the just created row ID in insertedRowID on success
  18027. // - does nothing by default, and returns false, i.e. always fails
  18028. function Insert(aRowID: Int64; var Values: TSQLVarDynArray;
  18029. out insertedRowID: Int64): boolean; virtual;
  18030. /// called to update a virtual table row content from an array of TSQLVar
  18031. // - should return true on success, false otherwise
  18032. // - does nothing by default, and returns false, i.e. always fails
  18033. function Update(oldRowID, newRowID: Int64; var Values: TSQLVarDynArray): boolean; virtual;
  18034. /// called to begin a transaction to the virtual table row
  18035. // - do nothing by default, and returns false in case of RollBack/RollBackto
  18036. // - aSavePoint is used for vttSavePoint, vttRelease and vttRollBackTo only
  18037. // - note that if you don't nest your writing within a transaction, SQLite
  18038. // will call vttCommit for each INSERT/UPDATE/DELETE, just like a regular
  18039. // SQLite database - it could make bad written code slow even with Virtual
  18040. // Tables
  18041. function Transaction(aState: TSQLVirtualTableTransaction; aSavePoint: integer): boolean; virtual;
  18042. /// called to rename the virtual table
  18043. // - by default, returns false, i.e. always fails
  18044. function Rename(const NewName: RawUTF8): boolean; virtual;
  18045. /// the associated virtual table storage instance
  18046. // - can be e.g. a TSQLRestStorageInMemory for TSQLVirtualTableJSON,
  18047. // or a TSQLRestStorageExternal for TSQLVirtualTableExternal, or nil
  18048. // for TSQLVirtualTableLog
  18049. property Static: TSQLRest read fStatic;
  18050. /// the associated virtual table storage instance, if is a TSQLRestStorage
  18051. property StaticStorage: TSQLRestStorage read fStaticStorage;
  18052. /// the associated virtual table storage table
  18053. property StaticTable: TSQLRecordClass read fStaticTable;
  18054. /// the associated virtual table storage index in its Model.Tables[] array
  18055. property StaticTableIndex: integer read fStaticTableIndex;
  18056. end;
  18057. /// abstract class able to define a Virtual Table cursor
  18058. // - override the Search/HasData/Column/Next abstract virtual methods to
  18059. // implement the search process
  18060. TSQLVirtualTableCursor = class
  18061. protected
  18062. fTable: TSQLVirtualTable;
  18063. /// used internaly between two Column() method calls for GetFieldSQLVar()
  18064. fColumnTemp: RawByteString;
  18065. /// easy set a TSQLVar content for the Column() method
  18066. procedure SetColumn(var aResult: TSQLVar; aValue: Int64); overload;
  18067. {$ifdef HASINLINE}inline;{$endif}
  18068. procedure SetColumn(var aResult: TSQLVar; const aValue: double); overload;
  18069. {$ifdef HASINLINE}inline;{$endif}
  18070. procedure SetColumn(var aResult: TSQLVar; const aValue: RawUTF8); overload;
  18071. {$ifdef HASINLINE}inline;{$endif}
  18072. procedure SetColumn(var aResult: TSQLVar; aValue: PUTF8Char; aValueLength: integer); overload;
  18073. {$ifdef HASINLINE}inline;{$endif}
  18074. procedure SetColumnBlob(var aResult: TSQLVar; aValue: pointer; aValueLength: integer);
  18075. {$ifdef HASINLINE}inline;{$endif}
  18076. public
  18077. /// create the cursor instance
  18078. // - it will be destroyed when by the DB engine (e.g. via xClose in SQLite3)
  18079. constructor Create(aTable: TSQLVirtualTable); virtual;
  18080. /// the associated Virtual Table class instance
  18081. property Table: TSQLVirtualTable read fTable;
  18082. public { abstract methods to be overridden }
  18083. /// called to begin a search in the virtual table
  18084. // - the TSQLVirtualTablePrepared parameters were set by
  18085. // TSQLVirtualTable.Prepare and will contain both WHERE and ORDER BY statements
  18086. // (retrieved e.g. by x_BestIndex() from a TSQLite3IndexInfo structure)
  18087. // - Prepared will contain all prepared constraints and the corresponding
  18088. // expressions in the Where[].Value field
  18089. // - should move cursor to first row of matching data
  18090. // - should return false on low-level database error (but true in case of a
  18091. // valid call, even if HasData will return false, i.e. no data match)
  18092. function Search(const Prepared: TSQLVirtualTablePrepared): boolean; virtual; abstract;
  18093. /// called after Search() to check if there is data to be retrieved
  18094. // - should return false if reached the end of matching data
  18095. function HasData: boolean; virtual; abstract;
  18096. /// called to retrieve a column value of the current data row into a TSQLVar
  18097. // - if aColumn=-1, should return the row ID as varInt64 into aResult
  18098. // - should return false in case of an error, true on success
  18099. function Column(aColumn: integer; var aResult: TSQLVar): boolean; virtual; abstract;
  18100. /// called to go to the next row of matching data
  18101. // - should return false on low-level database error (but true in case of a
  18102. // valid call, even if HasData will return false, i.e. no data match)
  18103. function Next: boolean; virtual; abstract;
  18104. end;
  18105. /// A generic Virtual Table cursor associated to Current/Max index properties
  18106. TSQLVirtualTableCursorIndex = class(TSQLVirtualTableCursor)
  18107. protected
  18108. fCurrent: integer;
  18109. fMax: integer;
  18110. public
  18111. /// called after Search() to check if there is data to be retrieved
  18112. // - will return false if reached the end of matching data, according to
  18113. // the fCurrent/fMax protected properties values
  18114. function HasData: boolean; override;
  18115. /// called to go to the next row of matching data
  18116. // - will return false on low-level database error (but true in case of a
  18117. // valid call, even if HasData will return false, i.e. no data match)
  18118. // - will check the fCurrent/fMax protected properties values
  18119. function Next: boolean; override;
  18120. /// called to begin a search in the virtual table
  18121. // - this no-op version will mark EOF, i.e. fCurrent=0 and fMax=-1
  18122. function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override;
  18123. end;
  18124. /// A Virtual Table cursor for reading a TSQLRestStorageInMemory content
  18125. // - this is the cursor class associated to TSQLVirtualTableJSON
  18126. TSQLVirtualTableCursorJSON = class(TSQLVirtualTableCursorIndex)
  18127. public
  18128. /// called to begin a search in the virtual table
  18129. // - the TSQLVirtualTablePrepared parameters were set by
  18130. // TSQLVirtualTable.Prepare and will contain both WHERE and ORDER BY statements
  18131. // (retrieved by x_BestIndex from a TSQLite3IndexInfo structure)
  18132. // - Prepared will contain all prepared constraints and the corresponding
  18133. // expressions in the Where[].Value field
  18134. // - will move cursor to first row of matching data
  18135. // - will return false on low-level database error (but true in case of a
  18136. // valid call, even if HasData will return false, i.e. no data match)
  18137. // - only handled WHERE clause is for "ID = value" - other request will
  18138. // return all records in ID order, and let the database engine handle it
  18139. function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override;
  18140. /// called to retrieve a column value of the current data row into a TSQLVar
  18141. // - if aColumn=-1, will return the row ID as varInt64 into aResult
  18142. // - will return false in case of an error, true on success
  18143. function Column(aColumn: integer; var aResult: TSQLVar): boolean; override;
  18144. end;
  18145. /// A TSQLRestStorageInMemory-based virtual table using JSON storage
  18146. // - for ORM access, you should use TSQLModel.VirtualTableRegister method to
  18147. // associated this virtual table module to a TSQLRecordVirtualTableAutoID class
  18148. // - transactions are not handled by this module
  18149. // - by default, no data is written on disk: you will need to call explicitly
  18150. // aServer.StaticVirtualTable[aClass].UpdateToFile for file creation or refresh
  18151. // - file extension is set to '.json'
  18152. TSQLVirtualTableJSON = class(TSQLVirtualTable)
  18153. protected
  18154. fStaticInMemory: TSQLRestStorageInMemory;
  18155. public { overridden methods }
  18156. /// create the virtual table access instance
  18157. // - the created instance will be released when the virtual table will be
  18158. // disconnected from the DB connection (e.g. xDisconnect method for SQLite3)
  18159. // - shall raise an exception in case of invalid parameters (e.g. if the
  18160. // supplied module is not associated to a TSQLRestServer instance)
  18161. // - aTableName will be checked against the current aModule.Server.Model
  18162. // to retrieve the corresponding TSQLRecordVirtualTableAutoID class and
  18163. // create any associated Static: TSQLRestStorage instance
  18164. constructor Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8;
  18165. FieldCount: integer; Fields: PPUTF8CharArray); override;
  18166. /// returns the main specifications of the associated TSQLVirtualTableModule
  18167. // - this is a read/write table, without transaction, associated to the
  18168. // TSQLVirtualTableCursorJSON cursor type, with 'JSON' as module name
  18169. // - no particular class is supplied here, since it will depend on the
  18170. // associated Static instance
  18171. class procedure GetTableModuleProperties(
  18172. var aProperties: TVirtualTableModuleProperties); override;
  18173. /// called to determine the best way to access the virtual table
  18174. // - will prepare the request for TSQLVirtualTableCursor.Search()
  18175. // - only prepared WHERE statement is for "ID = value"
  18176. // - only prepared ORDER BY statement is for ascending IDs
  18177. function Prepare(var Prepared: TSQLVirtualTablePrepared): boolean; override;
  18178. /// called when a DROP TABLE statement is executed against the virtual table
  18179. // - returns true on success, false otherwise
  18180. function Drop: boolean; override;
  18181. /// called to delete a virtual table row
  18182. // - returns true on success, false otherwise
  18183. function Delete(aRowID: Int64): boolean; override;
  18184. /// called to insert a virtual table row content from a TSQLVar array
  18185. // - column order follows the Structure method, i.e.
  18186. // StoredClassRecordProps.Fields[] order
  18187. // - returns true on success, false otherwise
  18188. // - returns the just created row ID in insertedRowID on success
  18189. // - does nothing by default, and returns false, i.e. always fails
  18190. function Insert(aRowID: Int64; var Values: TSQLVarDynArray;
  18191. out insertedRowID: Int64): boolean; override;
  18192. /// called to update a virtual table row content from a TSQLVar array
  18193. // - column order follows the Structure method, i.e.
  18194. // StoredClassRecordProps.Fields[] order
  18195. // - returns true on success, false otherwise
  18196. // - does nothing by default, and returns false, i.e. always fails
  18197. function Update(oldRowID, newRowID: Int64; var Values: TSQLVarDynArray): boolean; override;
  18198. end;
  18199. /// A TSQLRestStorageInMemory-based virtual table using Binary storage
  18200. // - for ORM access, you should use TSQLModel.VirtualTableRegister method to
  18201. // associated this virtual table module to a TSQLRecordVirtualTableAutoID class
  18202. // - transactions are not handled by this module
  18203. // - by default, no data is written on disk: you will need to call explicitly
  18204. // aServer.StaticVirtualTable[aClass].UpdateToFile for file creation or refresh
  18205. // - binary format is more efficient in term of speed and disk usage than
  18206. // the JSON format implemented by TSQLVirtualTableJSON
  18207. // - binary format will be set by TSQLVirtualTableJSON.CreateTableInstance
  18208. // - file extension is set to '.data'
  18209. TSQLVirtualTableBinary = class(TSQLVirtualTableJSON);
  18210. /// Implements a read/only virtual table able to access a .log file, as created
  18211. // by TSynLog
  18212. // - to be used e.g. by a TSQLRecordLog_Log ('Log_' will identify this 'Log' module)
  18213. // - the .log file name will be specified by the Table Name, to which a '.log'
  18214. // file extension will be appended before loading it from the current directory
  18215. TSQLVirtualTableLog = class(TSQLVirtualTable)
  18216. protected
  18217. fLogFile: TSynLogFile;
  18218. public
  18219. /// returns the main specifications of the associated TSQLVirtualTableModule
  18220. // - this is a read only table, with transaction, associated to the
  18221. // TSQLVirtualTableCursorLog cursor type, with 'Log' as module name,
  18222. // and associated to TSQLRecordLog_Log table field layout
  18223. class procedure GetTableModuleProperties(
  18224. var aProperties: TVirtualTableModuleProperties); override;
  18225. /// creates the TSQLVirtualTable according to the supplied parameters
  18226. // - aTableName will be checked against the current aModule.Server.Model
  18227. // to retrieve the corresponding TSQLRecordVirtualTableAutoID class
  18228. constructor Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8;
  18229. FieldCount: integer; Fields: PPUTF8CharArray); override;
  18230. /// release the associated .log file mapping and all internal structures
  18231. destructor Destroy; override;
  18232. end;
  18233. /// A Virtual Table cursor for reading a TSynLogFile content
  18234. // - this is the cursor class associated to TSQLVirtualTableLog
  18235. TSQLVirtualTableCursorLog = class(TSQLVirtualTableCursorIndex)
  18236. public
  18237. /// called to begin a search in the virtual table
  18238. function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override;
  18239. /// called to retrieve a column value of the current data row as TSQLVar
  18240. function Column(aColumn: integer; var aResult: TSQLVar): boolean; override;
  18241. end;
  18242. /// Record associated to a Virtual Table implemented in Delphi, with ID
  18243. // forced at INSERT
  18244. // - will use TSQLVirtualTableModule / TSQLVirtualTable / TSQLVirtualTableCursor
  18245. // classes for a generic Virtual table mechanism on the Server side
  18246. // - call Model.VirtualTableRegister() before TSQLRestServer.Create on the
  18247. // Server side (not needed for Client) to associate such a record with a
  18248. // particular Virtual Table module, otherwise an exception will be raised:
  18249. // ! Model.VirtualTableRegister(TSQLRecordDali1,TSQLVirtualTableJSON);
  18250. TSQLRecordVirtualTableForcedID = class(TSQLRecordVirtual);
  18251. /// Record associated to Virtual Table implemented in Delphi, with ID
  18252. // generated automatically at INSERT
  18253. // - will use TSQLVirtualTableModule / TSQLVirtualTable / TSQLVirtualTableCursor
  18254. // classes for a generic Virtual table mechanism
  18255. // - call Model.VirtualTableRegister() before TSQLRestServer.Create on the
  18256. // Server side (not needed for Client) to associate such a record with a
  18257. // particular Virtual Table module, otherwise an exception will be raised:
  18258. // ! Model.VirtualTableRegister(TSQLRecordDali1,TSQLVirtualTableJSON);
  18259. TSQLRecordVirtualTableAutoID = class(TSQLRecordVirtual);
  18260. /// special comparaison function for sorting ftRecord (TRecordReference/RecordRef)
  18261. // UTF-8 encoded values in the SQLite3 database or JSON content
  18262. function UTF8CompareRecord(P1,P2: PUTF8Char): PtrInt;
  18263. /// special comparaison function for sorting sftBoolean
  18264. // UTF-8 encoded values in the SQLite3 database or JSON content
  18265. function UTF8CompareBoolean(P1,P2: PUTF8Char): PtrInt;
  18266. /// special comparaison function for sorting sftEnumerate, sftSet or sftID
  18267. // UTF-8 encoded values in the SQLite3 database or JSON content
  18268. function UTF8CompareUInt32(P1,P2: PUTF8Char): PtrInt;
  18269. /// special comparaison function for sorting sftInteger, sftTID, sftRecordVersion
  18270. // or sftTimeLog / sftModTime / sftCreateTime UTF-8 encoded values in the SQLite3
  18271. // database or JSON content
  18272. function UTF8CompareInt64(P1,P2: PUTF8Char): PtrInt;
  18273. /// special comparaison function for sorting sftCurrency
  18274. // UTF-8 encoded values in the SQLite3 database or JSON content
  18275. function UTF8CompareCurr64(P1,P2: PUTF8Char): PtrInt;
  18276. /// special comparaison function for sorting sftFloat
  18277. // UTF-8 encoded values in the SQLite3 database or JSON content
  18278. function UTF8CompareDouble(P1,P2: PUTF8Char): PtrInt;
  18279. /// special comparaison function for sorting sftDateTime
  18280. // UTF-8 encoded values in the SQLite3 database or JSON content
  18281. function UTF8CompareISO8601(P1,P2: PUTF8Char): PtrInt;
  18282. {$ifndef NOVARIANTS}
  18283. /// low-level function used to convert a JSON Value into a variant,
  18284. // according to the property type
  18285. // - for sftObject, sftVariant, sftBlobDynArray and sftUTF8Custom, the
  18286. // JSON buffer may be an array or an object, so createValueTempCopy can
  18287. // create a temporary copy before parsing it in-place, to preserve the buffer
  18288. // - sftUnknown and sftMany would set a varEmpty (Unassigned) value
  18289. // - typeInfo may be used for sftBlobDynArray conversion to a TDocVariant array
  18290. procedure ValueVarToVariant(Value: PUTF8Char; fieldType: TSQLFieldType;
  18291. var result: TVarData; createValueTempCopy: boolean; typeInfo: pointer;
  18292. options: TDocVariantOptions=JSON_OPTIONS_FAST);
  18293. /// read an object properties from a TDocVariant object document
  18294. // - ObjectInstance must be an existing TObject instance
  18295. // - will return TRUE on success, or FALSE if the supplied aDocVariant was
  18296. // not a TDocVariant object
  18297. function ObjectLoadVariant(var ObjectInstance; const aDocVariant: variant;
  18298. TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean;
  18299. {$endif NOVARIANTS}
  18300. /// may be used by DatabaseExecute/AdministrationExecute methods to serve
  18301. // a folder content for remote administration
  18302. procedure AdministrationExecuteGetFiles(const Folder, Mask: TFileName; const Param: RawUTF8;
  18303. var Answer: TServiceCustomAnswer);
  18304. const
  18305. /// if a TSQLVirtualTablePreparedConstraint.Column is to be ignored
  18306. VIRTUAL_TABLE_IGNORE_COLUMN = -2;
  18307. /// if a TSQLVirtualTablePreparedConstraint.Column points to the RowID
  18308. VIRTUAL_TABLE_ROWID_COLUMN = -1;
  18309. /// if the TSQLRecordVirtual table kind is a FTS3/FTS4 virtual table
  18310. IS_FTS = [rFTS3, rFTS4];
  18311. /// if the TSQLRecordVirtual table kind is not an embedded type
  18312. // - can be set for a TSQLRecord after a VirtualTableExternalRegister call
  18313. IS_CUSTOM_VIRTUAL = [rCustomForcedID, rCustomAutoID];
  18314. /// if the TSQLRecordVirtual table kind expects the ID to be set on INSERT
  18315. INSERT_WITH_ID = [rFTS3, rFTS4, rRTree, rCustomForcedID];
  18316. /// Supervisor Table access right, i.e. alllmighty over all fields
  18317. ALL_ACCESS_RIGHTS = [0..MAX_SQLTABLES-1];
  18318. /// Complete Database access right, i.e. allmighty over all Tables
  18319. // - WITH the possibility to remotely execute any SQL statement (reSQL right)
  18320. // - is used by default by TSQLRestClientDB.URI() method, i.e. for direct
  18321. // local/in-process access
  18322. // - is used as reference to create TSQLAuthUser 'Admin' access policy
  18323. FULL_ACCESS_RIGHTS: TSQLAccessRights =
  18324. (AllowRemoteExecute:
  18325. [reSQL,reSQLSelectWithoutTable,reService,reUrlEncodedSQL,reUrlEncodedDelete];
  18326. GET: ALL_ACCESS_RIGHTS; POST: ALL_ACCESS_RIGHTS;
  18327. PUT: ALL_ACCESS_RIGHTS; DELETE: ALL_ACCESS_RIGHTS);
  18328. /// Supervisor Database access right, i.e. allmighty over all Tables
  18329. // - but WITHOUT the possibility to remotely execute any SQL statement (reSQL)
  18330. // - is used as reference to create TSQLAuthUser 'Supervisor' access policy
  18331. SUPERVISOR_ACCESS_RIGHTS: TSQLAccessRights =
  18332. (AllowRemoteExecute:
  18333. [reSQLSelectWithoutTable,reService,reUrlEncodedSQL,reUrlEncodedDelete];
  18334. GET: ALL_ACCESS_RIGHTS; POST: ALL_ACCESS_RIGHTS;
  18335. PUT: ALL_ACCESS_RIGHTS; DELETE: ALL_ACCESS_RIGHTS);
  18336. /// special TSQLFieldBits value containing all field bits set to 1
  18337. ALL_FIELDS: TSQLFieldBits = [0..MAX_SQLFIELDS-1];
  18338. // contains TSQLAuthUser.ComputeHashedPassword('synopse')
  18339. DEFAULT_HASH_SYNOPSE = '67aeea294e1cb515236fd7829c55ec820ef888e8e221814d24d83b3dc4d825dd';
  18340. /// the Server-side instance implementation patterns without any ID
  18341. SERVICE_IMPLEMENTATION_NOID = [sicSingle,sicShared];
  18342. /// typical TJSONSerializerSQLRecordOptions values for AJAX clients
  18343. JSONSERIALIZEROPTIONS_AJAX = [jwoAsJsonNotAsString,jwoID_str];
  18344. var
  18345. /// default hashed password set by TSQLAuthGroup.InitializeTable for 'Admin' user
  18346. // - you can override this value to follow your own application expectations
  18347. AuthAdminDefaultPassword: RawUTF8 = DEFAULT_HASH_SYNOPSE;
  18348. /// default hashed password set by TSQLAuthGroup.InitializeTable for 'Supervisor' user
  18349. // - you can override this value to follow your own application expectations
  18350. AuthSupervisorDefaultPassword: RawUTF8 = DEFAULT_HASH_SYNOPSE;
  18351. /// default hashed password set by TSQLAuthGroup.InitializeTable for 'User' user
  18352. // - you can override this value to follow your own application expectations
  18353. AuthUserDefaultPassword: RawUTF8 = DEFAULT_HASH_SYNOPSE;
  18354. const
  18355. /// timer identifier which indicates we must refresh the current Page
  18356. // - used for User Interface generation
  18357. // - is associated with the TSQLRibbonTabParameters.AutoRefresh property,
  18358. // and is handled in TSQLRibbon.RefreshClickHandled
  18359. WM_TIMER_REFRESH_SCREEN = 1;
  18360. /// timer identifier which indicates we must refresh the Report content
  18361. // - used for User Interface generation
  18362. // - is handled in TSQLRibbon.RefreshClickHandled
  18363. WM_TIMER_REFRESH_REPORT = 2;
  18364. /// the default URI parameters for query paging
  18365. // - those values are the one expected by YUI components
  18366. PAGINGPARAMETERS_YAHOO: TSQLRestServerURIPagingParameters = (
  18367. Sort: 'SORT=';
  18368. Dir: 'DIR=';
  18369. StartIndex: 'STARTINDEX=';
  18370. Results: 'RESULTS=';
  18371. Select: 'SELECT=';
  18372. Where: 'WHERE=';
  18373. SendTotalRowsCountFmt: '');
  18374. /// options to specify no index createon for TSQLRestServer.CreateMissingTables
  18375. // and TSQLRecord.InitializeTable methods
  18376. INITIALIZETABLE_NOINDEX: TSQLInitializeTableOptions =
  18377. [itoNoIndex4ID..itoNoIndex4RecordVersion];
  18378. /// default value of TSQLRestServer.StatLevels property
  18379. // - i.e. gather all statistics, but mlSessions
  18380. SERVERDEFAULTMONITORLEVELS: TSQLRestServerMonitorLevels =
  18381. [mlTables,mlMethods,mlInterfaces,mlSQLite3];
  18382. /// wrapper to search for a given TSQLRecord by ID in an array of TSQLRecord
  18383. function ObjArraySearch(const aSQLRecordObjArray; aID: TID): TSQLRecord;
  18384. /// wrapper to return all TID values of an array of TSQLRecord
  18385. procedure ObjArrayRecordIDs(const aSQLRecordObjArray; out result: TInt64DynArray);
  18386. /// safe deletion of a T*InterfaceArray dynamic array item
  18387. // - similar to InterfaceArrayDelete, but with a safe try .. except block
  18388. // during the entry deletion (since the system may be unstable)
  18389. // - will also log a warning with the Interface name (from aLogMsg) and aInstance
  18390. procedure InterfaceArrayDeleteAfterException(var aInterfaceArray;
  18391. const aItemIndex: integer; aLog: TSynLogFamily; const aLogMsg: RawUTF8; aInstance: TObject);
  18392. /// create a TRecordReference with the corresponding parameters
  18393. function RecordReference(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID): TRecordReference; overload;
  18394. /// create a TRecordReference with the corresponding parameters
  18395. function RecordReference(aTableIndex: cardinal; aID: TID): TRecordReference; overload;
  18396. {$ifdef HASINLINE}inline;{$endif}
  18397. /// convert a dynamic array of TRecordReference into its corresponding IDs
  18398. procedure RecordRefToID(var aArray: TInt64DynArray);
  18399. /// get the order table name from a SQL statement
  18400. // - return the word following any 'ORDER BY' statement
  18401. // - return 'RowID' if none found
  18402. function SQLGetOrder(const SQL: RawUTF8): RawUTF8;
  18403. {$ifdef PUREPASCAL}{$ifdef HASINLINE}
  18404. /// this function is published only for class function TSQLRecord.RecordProps()
  18405. // internal call after inlining
  18406. function PropsCreate(aTable: TSQLRecordClass): TSQLRecordProperties;
  18407. {$endif}{$endif}
  18408. /// low-level function to retrieve the class instance implementing a given
  18409. // interface
  18410. // - this will work with interfaces stubs generated by the compiler, but also
  18411. // with TInterfaceFactory.CreateFakeInstance kind of classes
  18412. function ObjectFromInterface(const aValue: IInterface): TObject;
  18413. /// low-level function to check if a class instance, retrieved from its
  18414. // interface variable, does in fact implement a given interface
  18415. // - this will call ObjectFromInterface(), so will work with interfaces
  18416. // stubs generated by the compiler, but also with
  18417. // TInterfaceFactory.CreateFakeInstance kind of classes
  18418. function ObjectFromInterfaceImplements(const aValue: IInterface;
  18419. const aInterface: TGUID): boolean;
  18420. /// assign a Weak interface reference, to be used for circular references
  18421. // - by default setting aInterface.Field := aValue will increment the internal
  18422. // reference count of the implementation object: when underlying objects reference
  18423. // each other via interfaces (e.g. as parent and children), what causes the
  18424. // reference count to never reach zero, therefore resulting in memory links
  18425. // - to avoid this issue, use this procedure instead
  18426. procedure SetWeak(aInterfaceField: PIInterface; const aValue: IInterface);
  18427. // {$ifdef HASINLINE}inline;{$endif} raise compilation Internal Error C2170
  18428. /// assign a Weak interface reference, which will be ZEROed (set to nil) when
  18429. // the corresponding object will be released
  18430. // - this function is bit slower than SetWeak, but will avoid any GPF, by
  18431. // maintaining a list of per-instance weak interface field reference, and
  18432. // hook the FreeInstance virtual method in order to reset any reference to nil:
  18433. // FreeInstance will be overridden for this given class VMT only (to avoid
  18434. // unnecessary slowdown of other classes), calling the previous method afterward
  18435. // (so will work even with custom FreeInstance implementations)
  18436. // - for faster possible retrieval, it will assign the unused vmtAutoTable VMT
  18437. // entry trick (just like TSQLRecord.RecordProps) - note that it will be
  18438. // compatible also with interfaces implemented via TSQLRecord children
  18439. // - thread-safe implementation, using a per-class fast lock
  18440. procedure SetWeakZero(aObject: TObject; aObjectInterfaceField: PIInterface;
  18441. const aValue: IInterface);
  18442. {$ifdef ISDELPHIXE} // class helper requires Delphi 2006 or newer but are buggy before XE :(
  18443. type
  18444. /// TWeakZeroInterfaceHelper is a class helper that allows you to use
  18445. // SetWeakZero() in any class without specifying the Self parameter
  18446. TWeakZeroInterfaceHelper = class helper for TObject
  18447. protected
  18448. /// Use SetWeak0 to assign an interface to a weak interface field
  18449. // - this is just a wrapper around the global SetWeakZero() function
  18450. procedure SetWeak0(aObjectInterfaceField: PIInterface; const aValue: IInterface);
  18451. end;
  18452. {$endif}
  18453. var
  18454. /// if this variable is TRUE, the URIRequest() function won't use
  18455. // Win32 API GlobalAlloc() function, but fastest native Getmem()
  18456. // - can be also useful for debugg
  18457. USEFASTMM4ALLOC: boolean = false;
  18458. /// this function can be exported from a DLL to remotely access to a TSQLRestServer
  18459. // - use TSQLRestServer.ExportServer to assign a server to this function
  18460. // - return 501 HTML_NOTIMPLEMENTED if no TSQLRestServer.ExportServer has been assigned
  18461. // - memory for Resp and Head are allocated with GlobalAlloc(): client must release
  18462. // this pointers with GlobalFree() after having retrieved their content
  18463. // - simply use TSQLRestClientURIDll to access to an exported URIRequest() function
  18464. function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl;
  18465. threadvar
  18466. /// this thread-specific variable will be set with the currently running
  18467. // service context (on the server side)
  18468. // - note that in case of direct server side execution of the service, this
  18469. // information won't be filled, so the safest (and slightly faster) access
  18470. // to the TSQLRestServer instance associated with a service is to inherit your
  18471. // implementation class from TInjectableObjectRest, and not use this threadvar
  18472. // - is set by TServiceFactoryServer.ExecuteMethod() just before calling the
  18473. // implementation method of a service, allowing to retrieve the current
  18474. // execution context - Request member is set from a client/server execution:
  18475. // Request.Server is the safe access point to the underlying TSQLRestServer,
  18476. // in such context - also consider the CurrentServiceContextServer function to
  18477. // retrieve directly the running TSQLRestServer (if any)
  18478. // - its content is reset to zero out of the scope of a method execution
  18479. // - when used, a local copy or a PServiceRunningContext pointer should better
  18480. // be created, since accessing a threadvar has a non negligible performance
  18481. // cost - for instance, if you want to use a "with" statement:
  18482. // ! with PServiceRunningContext(@ServiceContext)^ do
  18483. // ! ... access TServiceRunningContext members
  18484. // or as a local variable:
  18485. // !var context: PServiceRunningContext;
  18486. // ! inContentType: RawUTF8;
  18487. // !begin
  18488. // ! context := @ServiceContext; // threadvar access once
  18489. // ! ...
  18490. // ! inContentType := FindIniNameValue(pointer(context.Request.Call^.InHead),
  18491. // ! HEADER_CONTENT_TYPE_UPPER);
  18492. // !end;
  18493. // - when accessed from a package, use function CurrentServiceContext()
  18494. // instead, to circumvent a Delphi RTL/compiler restriction (bug?)
  18495. ServiceContext: TServiceRunningContext;
  18496. /// wrapper function to retrieve the global ServiceContext threadvar value
  18497. // - to be used when accessing the value from a package, to circumvent a
  18498. // Delphi RTL/compiler restriction (bug?)
  18499. function CurrentServiceContext: TServiceRunningContext;
  18500. /// wrapper function to retrieve the current REST server instance from
  18501. // the global ServiceContext threadvar value
  18502. // - may return nil if ServiceContext.Request is nil: in this case,
  18503. // you should better implement your service by inheriting the implementation
  18504. // class from TInjectableObjectRest
  18505. function CurrentServiceContextServer: TSQLRestServer;
  18506. function ToText(ft: TSQLFieldType): PShortString; overload;
  18507. function ToText(tk: TTypeKind): PShortString; overload;
  18508. function ToText(e: TSQLEvent): PShortString; overload;
  18509. function ToText(he: TSQLHistoryEvent): PShortString; overload;
  18510. function ToText(o: TSQLOccasion): PShortString; overload;
  18511. function ToText(dft: TSQLDBFieldType): PShortString; overload;
  18512. function ToText(si: TServiceInstanceImplementation): PShortString; overload;
  18513. function ToText(cmd: TSQLRestServerURIContextCommand): PShortString; overload;
  18514. function ToText(op: TSQLQueryOperator): PShortString; overload;
  18515. function ToText(V: TInterfaceMockSpyCheck): PShortString; overload;
  18516. function ToText(m: TSQLURIMethod): PShortString; overload;
  18517. function ToText(o: TSynTableStatementOperator): PShortString; overload;
  18518. function ToText(t: TSQLVirtualTableTransaction): PShortString; overload;
  18519. { ************ Logging classes and functions }
  18520. type
  18521. /// logging class with enhanced RTTI
  18522. // - will write TObject/TSQLRecord, enumerations and sets content as JSON
  18523. // - is the default logging family used by the mORMot framework
  18524. // - mORMotDB.pas unit will set SynDBLog := TSQLLog
  18525. // - mORMotSQLite3.pas unit will set SynSQLite3Log := TSQLLog
  18526. TSQLLog = class(TSynLog)
  18527. protected
  18528. procedure CreateLogWriter; override;
  18529. end;
  18530. {$ifdef WITHLOG}
  18531. var
  18532. /// TSQLLog class is used for logging for all our ORM related functions
  18533. // - this global variable can be used to customize it for the whole process
  18534. // - each TSQLRest.LogClass property is set by default to this SQLite3Log
  18535. // - you can override the TSQLRest.LogClass property value to customize it
  18536. // for a given REST instance
  18537. SQLite3Log: TSynLogClass = TSQLLog;
  18538. /// TSQLogClass used by overriden SetThreadName() function to name the thread
  18539. SetThreadNameLog: TSynLogClass = TSQLLog;
  18540. {$endif}
  18541. implementation
  18542. uses
  18543. {$ifdef FPC}
  18544. {$ifndef MSWINDOWS}
  18545. SynFPCLinux,
  18546. BaseUnix,
  18547. Unix,
  18548. dynlibs,
  18549. {$endif}
  18550. {$endif}
  18551. SynCrypto; // for TSQLRecordSigned and authentication
  18552. // ************ some RTTI and SQL mapping routines
  18553. procedure SetID(P: PUTF8Char; var result: TID);
  18554. {$ifdef HASINLINE}
  18555. {$ifdef CPU64}
  18556. begin // PtrInt is already int64 -> call PtrInt version
  18557. result := GetInteger(P);
  18558. {$else}
  18559. begin
  18560. {$ifdef VER3_0} // FPC issue woraround
  18561. SetInt64(P,result);
  18562. {$else}
  18563. SetInt64(P,PInt64(@result)^);
  18564. {$endif}
  18565. {$endif}
  18566. {$else}
  18567. asm
  18568. jmp SynCommons.SetInt64
  18569. {$endif}
  18570. end;
  18571. procedure SetID(const U: RawByteString; var result: TID); overload;
  18572. {$ifdef HASINLINE}
  18573. {$ifdef CPU64}
  18574. begin // PtrInt is already int64 -> call PtrInt version
  18575. result := GetInteger(pointer(U));
  18576. {$else}
  18577. begin
  18578. SetID(pointer(U),result);
  18579. {$endif}
  18580. {$else}
  18581. asm
  18582. jmp SynCommons.SetInt64
  18583. {$endif}
  18584. end;
  18585. {$ifdef HASDIRECTTYPEINFO}
  18586. type
  18587. Deref = PTypeInfo;
  18588. {$else}
  18589. function Deref(Info: PPTypeInfo): PTypeInfo;
  18590. {$ifdef HASINLINE} inline;
  18591. begin
  18592. if Info=nil then
  18593. result := pointer(Info) else
  18594. result := Info^;
  18595. end;
  18596. {$else}
  18597. asm // Delphi is so bad at compiling above code...
  18598. test eax,eax
  18599. jz @z
  18600. mov eax,[eax]
  18601. ret
  18602. @z: db $f3 // rep ret
  18603. end;
  18604. {$endif HASINLINE}
  18605. {$endif HASDIRECTTYPEINFO}
  18606. {$ifndef FPC}
  18607. type
  18608. /// used to map a TPropInfo.GetProc/SetProc and retrieve its kind
  18609. PropWrap = packed record
  18610. FillBytes: array [0..SizeOf(Pointer)-2] of byte;
  18611. /// = $ff for a field address, or =$fe for a virtual method
  18612. Kind: byte;
  18613. end;
  18614. /// no Rtti alignment under Delphi
  18615. AlignToPtr = pointer;
  18616. UnalignToDouble = Double;
  18617. const
  18618. NO_INDEX = Integer($80000000);
  18619. {$endif FPC}
  18620. function GetInterfaceFromEntry(Instance: TObject; Entry: PInterfaceEntry; out Obj): boolean;
  18621. {$ifndef FPC}
  18622. procedure UseImplGetter(Instance: TObject; ImplGetter: PtrInt; var result: IInterface);
  18623. type // function(Instance: TObject) trick does not work with CPU64 :(
  18624. TGetProc = function: IInterface of object;
  18625. var Call: TMethod;
  18626. begin // sub-procedure to avoid try..finally for TGetProc(): Interface result
  18627. if PropWrap(ImplGetter).Kind=$FE then
  18628. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(ImplGetter))^ else
  18629. Call.Code := Pointer(ImplGetter);
  18630. Call.Data := Instance;
  18631. result := TGetProc(Call);
  18632. end;
  18633. {$endif}
  18634. begin
  18635. Pointer(Obj) := nil;
  18636. if Entry<>nil then
  18637. if Entry^.IOffset <> 0 then begin
  18638. Pointer(Obj) := Pointer(PtrInt(Instance)+Entry^.IOffset);
  18639. if Pointer(Obj)<>nil then
  18640. IInterface(Obj)._AddRef;
  18641. end
  18642. {$ifndef FPC} else
  18643. if PropWrap(Entry^.ImplGetter).Kind=$FF then
  18644. IInterface(Obj) := IInterface(PPointer(PtrUInt(Instance)+PtrUInt(Entry^.ImplGetter and $00FFFFFF))^) else
  18645. UseImplGetter(Instance,Entry^.ImplGetter,IInterface(Obj)){$endif};
  18646. Result := Pointer(Obj)<>nil;
  18647. end;
  18648. function InternalMethodInfo(aClassType: TClass; const aMethodName: ShortString): PMethodInfo;
  18649. var Count, i: integer;
  18650. begin
  18651. while aClassType<>nil do begin
  18652. result := PPointer(PtrInt(aClassType)+vmtMethodTable)^;
  18653. if result<>nil then begin
  18654. {$ifdef FPC}
  18655. Count := PCardinal(result)^;
  18656. inc(PCardinal(result));
  18657. {$else}
  18658. Count := PWord(result)^;
  18659. inc(PWord(result));
  18660. {$endif}
  18661. for i := 0 to Count-1 do
  18662. if IdemPropName(result^.Name{$ifdef FPC}^{$endif},aMethodName) then
  18663. Exit else
  18664. {$ifdef FPC}
  18665. inc(result);
  18666. {$else}
  18667. inc(PByte(result),result^.Len);
  18668. {$endif}
  18669. end;
  18670. {$ifdef FPC}
  18671. aClassType := aClassType.ClassParent;
  18672. if aClassType=nil then
  18673. {$else}
  18674. if PPointer(PtrInt(aClassType)+vmtParent)^<>nil then
  18675. aClassType := PPointer(PPointer(PtrInt(aClassType)+vmtParent)^)^ else
  18676. {$endif}
  18677. break;
  18678. end;
  18679. result := nil;
  18680. end;
  18681. function TMethodInfo.MethodAddr: Pointer;
  18682. begin
  18683. if @self<>nil then
  18684. result := Addr else
  18685. result := @self;
  18686. end;
  18687. function TMethodInfo.ReturnInfo: PReturnInfo;
  18688. begin // see http://hallvards.blogspot.fr/2006/09/extended-class-rtti.html
  18689. if @self<>nil then begin
  18690. {$ifdef FPC}
  18691. result := pointer(PtrUInt(@Addr)+sizeof(Pointer));
  18692. {$else}
  18693. result := @Name[ord(Name[0])+1];
  18694. if PtrUInt(result)-PtrUInt(@self)=Len then
  18695. result := nil; // no method details available
  18696. {$endif}
  18697. end else
  18698. result := @self;
  18699. end;
  18700. function TReturnInfo.Param: PParamInfo;
  18701. begin
  18702. result := Pointer(PtrUInt(@self)+sizeof(TReturnInfo));
  18703. end;
  18704. function TParamInfo.Next: PParamInfo;
  18705. begin
  18706. result := AlignToPtr(@Name[ord(Name[0])+1]);
  18707. {$ifdef ISDELPHI2010}
  18708. Inc(PByte(result),PWord(result)^); // attributes
  18709. {$endif}
  18710. end;
  18711. function InternalClassProp(ClassType: TClass): PClassProp;
  18712. {$ifdef FPC}
  18713. begin
  18714. with GetFPCTypeData(ClassType.ClassInfo)^ do
  18715. result := AlignToPtr(@UnitName[ord(UnitName[0])+1]);
  18716. {$else}
  18717. {$ifdef PUREPASCAL}
  18718. var PTI: PTypeInfo;
  18719. begin // code is a bit abstract, but compiles very well
  18720. PTI := PPointer(PtrInt(ClassType)+vmtTypeInfo)^;
  18721. if PTI<>nil then // avoid GPF if no RTTI available for this class
  18722. with PTI^, PClassType(@Name[ord(Name[0])+1])^ do
  18723. result := PClassProp(@UnitName[ord(UnitName[0])+1]) else
  18724. result := nil;
  18725. {$else}
  18726. asm // this code is the fastest possible
  18727. mov eax,[eax+vmtTypeInfo]
  18728. test eax,eax; jz @z // avoid GPF if no RTTI available for this class
  18729. movzx edx,byte ptr [eax].TTypeInfo.Name
  18730. lea eax,[eax+edx].TTypeInfo.Name[1]
  18731. movzx edx,byte ptr [eax].TClassType.UnitName
  18732. lea eax,[eax+edx].TClassType.UnitName[1].TClassProp
  18733. @z:
  18734. {$endif PUREPASCAL}
  18735. {$endif FPC}
  18736. end;
  18737. function InternalClassPropInfo(ClassType: TClass; out PropInfo: PPropInfo): integer;
  18738. {$ifdef FPC}
  18739. var CP: PClassProp;
  18740. {$endif}
  18741. begin
  18742. if ClassType<>nil then begin
  18743. {$ifdef FPC}
  18744. CP := InternalClassProp(ClassType);
  18745. if CP<>nil then begin // no more RTTI information available
  18746. PropInfo := AlignToPtr(@CP^.PropList);
  18747. result := CP^.PropCount;
  18748. {$else} // code is a bit abstract, but compiles very well for Delphi/Kylix
  18749. inc(PByte(ClassType),vmtTypeInfo);
  18750. if PPointer(ClassType)^<>nil then // avoid GPF if no RTTI available
  18751. with PTypeInfo(PPointer(ClassType)^)^, PClassType(@Name[ord(Name[0])+1])^,
  18752. PClassProp(@UnitName[ord(UnitName[0])+1])^ do begin
  18753. PropInfo := @PropList;
  18754. result := PropCount;
  18755. {$endif}
  18756. exit;
  18757. end;
  18758. end;
  18759. result := 0;
  18760. end;
  18761. function ClassFieldCountWithParents(ClassType: TClass;
  18762. onlyWithoutGetter: boolean): integer;
  18763. var CP: PClassProp;
  18764. P: PPropInfo;
  18765. i: integer;
  18766. begin
  18767. result := 0;
  18768. while ClassType<>nil do begin
  18769. CP := InternalClassProp(ClassType);
  18770. if CP=nil then
  18771. break; // no RTTI information (e.g. reached TObject level)
  18772. if onlyWithoutGetter then begin
  18773. P := AlignToPtr(@CP^.PropList);
  18774. for i := 1 to CP^.PropCount do begin
  18775. if P^.GetterIsField then
  18776. inc(result);
  18777. P := P^.Next;
  18778. end;
  18779. end else
  18780. inc(result,CP^.PropCount);
  18781. ClassType := ClassType.ClassParent;
  18782. end;
  18783. end;
  18784. function ClassHasPublishedFields(ClassType: TClass): boolean;
  18785. var CP: PClassProp;
  18786. begin
  18787. while ClassType<>nil do begin
  18788. CP := InternalClassProp(ClassType);
  18789. if CP=nil then
  18790. break; // no RTTI information (e.g. reached TObject level)
  18791. if CP^.PropCount>0 then begin
  18792. result := true;
  18793. exit;
  18794. end;
  18795. ClassType := ClassType.ClassParent;
  18796. end;
  18797. result := false;
  18798. end;
  18799. function ClassHierarchyWithField(ClassType: TClass): TClassDynArray;
  18800. procedure InternalAdd(C: TClass; var list: TClassDynArray);
  18801. var P: PClassProp;
  18802. begin
  18803. if C=nil then
  18804. exit;
  18805. InternalAdd(C.ClassParent,list);
  18806. P := InternalClassProp(C);
  18807. if (P<>nil) and (P^.PropCount>0) then
  18808. ObjArrayAdd(list,pointer(C));
  18809. end;
  18810. begin
  18811. result := nil;
  18812. InternalAdd(ClassType,result);
  18813. end;
  18814. function ClassFieldAllProps(ClassType: TClass; Types: TTypeKinds): PPropInfoDynArray;
  18815. var CP: PClassProp;
  18816. P: PPropInfo;
  18817. i,n: integer;
  18818. begin
  18819. n := 0;
  18820. result := nil;
  18821. while ClassType<>nil do begin
  18822. CP := InternalClassProp(ClassType);
  18823. if CP=nil then
  18824. break; // no RTTI information (e.g. reached TObject level)
  18825. if CP^.PropCount>0 then begin
  18826. SetLength(result,n+CP^.PropCount);
  18827. P := AlignToPtr(@CP^.PropList);
  18828. for i := 1 to CP^.PropCount do begin
  18829. if P^.PropType^.Kind in Types then begin
  18830. result[n] := P;
  18831. inc(n);
  18832. end;
  18833. {$ifdef HASINLINE}
  18834. P := P^.Next;
  18835. {$else}
  18836. with P^ do P := @Name[ord(Name[0])+1];
  18837. {$endif}
  18838. end;
  18839. end;
  18840. ClassType := ClassType.ClassParent;
  18841. end;
  18842. SetLength(result,n);
  18843. end;
  18844. function ClassFieldNamesAllProps(ClassType: TClass; IncludePropType: boolean;
  18845. Types: TTypeKinds): TRawUTF8DynArray;
  18846. var props: PPropInfoDynArray;
  18847. n,i: integer;
  18848. begin
  18849. props := ClassFieldAllProps(ClassType,Types);
  18850. n := length(props);
  18851. SetLength(result,n);
  18852. for i := 0 to n-1 do
  18853. if IncludePropType then
  18854. FormatUTF8('%: %',[props[i]^.Name,props[i]^.PropType^.Name],result[i]) else
  18855. ShortStringToAnsi7String(props[i]^.Name,result[i]);
  18856. end;
  18857. function ClassFieldNamesAllPropsAsText(ClassType: TClass; IncludePropType: boolean;
  18858. Types: TTypeKinds): RawUTF8;
  18859. begin
  18860. result := RawUTF8ArrayToCSV(
  18861. ClassFieldNamesAllProps(ClassType,IncludePropType,Types),', ');
  18862. end;
  18863. function ClassFieldProp(ClassType: TClass; const PropName: shortstring): PPropInfo;
  18864. begin
  18865. if ClassType<>nil then
  18866. result := InternalClassProp(ClassType)^.FieldProp(PropName) else
  18867. result := nil;
  18868. end;
  18869. function ClassFieldPropWithParents(aClassType: TClass; const PropName: shortstring): PPropInfo;
  18870. var i: integer;
  18871. begin
  18872. while aClassType<>nil do begin
  18873. for i := 1 to InternalClassPropInfo(aClassType,result) do
  18874. if (result^.Name[0]=PropName[0]) and
  18875. IdemPropNameUSameLen(@result^.Name[1],@PropName[1],ord(PropName[0])) then
  18876. exit else
  18877. {$ifdef HASINLINE}
  18878. result := result^.Next;
  18879. {$else}
  18880. with result^ do result := @Name[ord(Name[0])+1];
  18881. {$endif}
  18882. aClassType := aClassType.ClassParent;
  18883. end;
  18884. result := nil;
  18885. end;
  18886. function ClassFieldPropWithParentsFromUTF8(aClassType: TClass; PropName: PUTF8Char;
  18887. PropNameLen: integer): PPropInfo;
  18888. {$ifndef FPC}
  18889. var i: integer;
  18890. {$endif}
  18891. begin
  18892. {$ifdef FPC}
  18893. result := pointer(GetFPCPropInfo(aClassType,PropName));
  18894. {$else}
  18895. while (PropNameLen<>0) and (aClassType<>nil) do begin
  18896. for i := 1 to InternalClassPropInfo(aClassType,result) do
  18897. if IdemPropName(result^.Name,PropName,PropNameLen) then
  18898. exit else
  18899. result := result^.Next;
  18900. aClassType := aClassType.ClassParent;
  18901. end;
  18902. result := nil;
  18903. {$endif}
  18904. end;
  18905. function ClassFieldPropWithParentsFromClassType(aClassType,aSearchedClassType: TClass): PPropInfo;
  18906. var i: integer;
  18907. begin
  18908. if aSearchedClassType<>nil then
  18909. while aClassType<>nil do begin
  18910. for i := 1 to InternalClassPropInfo(aClassType,result) do
  18911. if (result^.PropType^.Kind=tkClass) and
  18912. (result^.PropType^.ClassType^.ClassType=aSearchedClassType) then
  18913. exit else
  18914. result := result^.Next;
  18915. aClassType := aClassType.ClassParent;
  18916. end;
  18917. result := nil;
  18918. end;
  18919. function ClassFieldInstance(Instance: TObject; const PropName: shortstring;
  18920. PropClassType: TClass; out PropInstance): boolean;
  18921. var P: PPropInfo;
  18922. begin
  18923. result := false;
  18924. if Instance=nil then
  18925. exit;
  18926. P := ClassFieldPropWithParents(Instance.ClassType,PropName);
  18927. if (P=nil) or (P^.PropType^.Kind<>tkClass) or
  18928. not P^.PropType^.InheritsFrom(PropClassType) then
  18929. exit;
  18930. TObject(PropInstance) := P^.GetObjProp(Instance);
  18931. result := true;
  18932. end;
  18933. function ClassFieldInstance(Instance: TObject; PropClassType: TClass;
  18934. out PropInstance): boolean; overload;
  18935. var P: PPropInfo;
  18936. begin
  18937. result := false;
  18938. if Instance=nil then
  18939. exit;
  18940. P := ClassFieldPropWithParentsFromClassType(Instance.ClassType,PropClassType);
  18941. if P=nil then
  18942. exit;
  18943. TObject(PropInstance) := P^.GetObjProp(Instance);
  18944. result := true;
  18945. end;
  18946. function GetObjectComponent(Obj: TPersistent; const ComponentName: shortstring;
  18947. ComponentClass: TClass): pointer;
  18948. var P: PPropInfo;
  18949. begin
  18950. result := nil;
  18951. if Obj=nil then
  18952. exit;
  18953. P := ClassFieldPropWithParents(Obj.ClassType,ComponentName);
  18954. if (P<>nil) and (P^.PropType^.Kind=tkClass) then
  18955. if P^.PropType^.InheritsFrom(ComponentClass) then
  18956. result := P^.GetObjProp(Obj);
  18957. end;
  18958. function GetEnumCaption(aTypeInfo: PTypeInfo; const aIndex): string;
  18959. begin
  18960. if (aTypeInfo=nil) or (aTypeInfo^.Kind<>tkEnumeration) then
  18961. result := '' else
  18962. result := aTypeInfo^.EnumBaseType^.GetCaption(PByte(@aIndex)^);
  18963. end;
  18964. function GetEnumNameTrimed(aTypeInfo: PTypeInfo; const aIndex): RawUTF8;
  18965. begin
  18966. if (aTypeInfo=nil) or (aTypeInfo^.Kind<>tkEnumeration) then
  18967. result := '' else
  18968. result := aTypeInfo^.EnumBaseType^.GetEnumNameTrimed(aIndex);
  18969. end;
  18970. function GetSetNameCSV(aTypeInfo: PTypeInfo; const aValue): RawUTF8;
  18971. begin
  18972. if (aTypeInfo=nil) or (aTypeInfo^.Kind<>tkSet) then
  18973. result := '' else
  18974. result := aTypeInfo^.SetEnumType^.GetSetNameCSV(integer(aValue));
  18975. end;
  18976. function DocVariantToObject(var doc: TDocVariantData; obj: TObject): boolean;
  18977. var p: integer;
  18978. prop: PPropInfo;
  18979. begin
  18980. if (doc.Kind=dvObject) and (doc.Count>0) and (obj<>nil) then begin
  18981. for p := 0 to doc.Count-1 do begin
  18982. prop := ClassFieldPropWithParentsFromUTF8(
  18983. PPointer(obj)^,pointer(doc.Names[p]),length(doc.Names[p]));
  18984. if prop<>nil then
  18985. prop^.SetFromVariant(obj,doc.Values[p]);
  18986. end;
  18987. result := true;
  18988. end else
  18989. result := false;
  18990. end;
  18991. procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray;
  18992. objClass: TClass);
  18993. var instance: TClassInstance;
  18994. begin
  18995. instance.Init(objClass);
  18996. DocVariantToObjArray(arr,objArray,@instance);
  18997. end;
  18998. procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray;
  18999. objClass: PClassInstance);
  19000. var i: integer;
  19001. obj: TObjectDynArray absolute objArray;
  19002. begin
  19003. if objClass=nil then
  19004. exit;
  19005. ObjArrayClear(obj);
  19006. if (arr.Kind<>dvArray) or (arr.Count=0) then
  19007. exit;
  19008. SetLength(obj,arr.Count);
  19009. for i := 0 to arr.Count-1 do begin
  19010. obj[i] := objClass^.CreateNew;
  19011. DocVariantToObject(_Safe(arr.Values[i])^,obj[i]);
  19012. end;
  19013. end;
  19014. type // those classes will be used to register globally some classes for JSON
  19015. TJSONSerializerRegisteredClassAbstract = class(TList)
  19016. protected
  19017. fLastClass: TClass;
  19018. fSafe: TSynLocker;
  19019. public
  19020. constructor Create;
  19021. destructor Destroy; override;
  19022. end;
  19023. TJSONSerializerRegisteredClass = class(TJSONSerializerRegisteredClassAbstract)
  19024. protected
  19025. public
  19026. procedure AddOnce(aItemClass: TClass);
  19027. function Find(JSON: PUTF8Char; AndRegisterClass: boolean): TClass; overload;
  19028. function Find(aClassName: PUTF8Char; aClassNameLen: integer): TClass; overload;
  19029. end;
  19030. var
  19031. JSONSerializerRegisteredClass: TJSONSerializerRegisteredClass=nil;
  19032. { TSQLPropInfo }
  19033. const
  19034. NULL_SHORTSTRING: string[1] = '';
  19035. function TSQLPropInfo.GetSQLFieldTypeName: PShortString;
  19036. begin
  19037. if self=nil then
  19038. result := @NULL_SHORTSTRING else
  19039. result := ToText(fSQLFieldType);
  19040. end;
  19041. function TSQLPropInfo.GetSQLFieldRTTITypeName: RawUTF8;
  19042. begin
  19043. result := GetDisplayNameFromClass(ClassType);
  19044. if IdemPChar(pointer(result),'PROPINFO') then
  19045. delete(result,1,8);
  19046. end;
  19047. function TSQLPropInfo.GetNameDisplay: string;
  19048. begin
  19049. GetCaptionFromPCharLen(pointer(fName),result);
  19050. end;
  19051. procedure TSQLPropInfo.TextToBinary(Value: PUTF8Char; var result: RawByteString);
  19052. begin
  19053. result := BlobToTSQLRawBlob(Value);
  19054. end;
  19055. procedure TSQLPropInfo.BinaryToText(var Value: RawUTF8; ToSQL: boolean;
  19056. wasSQLString: PBoolean);
  19057. begin
  19058. if Value='' then begin
  19059. if wasSQLString<>nil then
  19060. wasSQLString^ := false;
  19061. Value := 'null';
  19062. end else begin
  19063. if wasSQLString<>nil then
  19064. wasSQLString^ := true;
  19065. if ToSQL then
  19066. // encode as BLOB literals (e.g. "X'53514C697465'")
  19067. Value := TSQLRawBlobToBlob(TSQLRawBlob(Value)) else
  19068. // JSON content is e.g. '\uFFF0base64encodedbinary'
  19069. Value := BinToBase64WithMagic(Value);
  19070. end;
  19071. end;
  19072. {$ifndef NOVARIANTS}
  19073. function NullableTypeToSQLFieldType(aType: pointer): TSQLFieldType;
  19074. begin
  19075. if aType<>nil then
  19076. if aType<>TypeInfo(TNullableInteger) then
  19077. if aType<>TypeInfo(TNullableUTF8Text) then
  19078. if aType<>TypeInfo(TNullableBoolean) then
  19079. if aType<>TypeInfo(TNullableFloat) then
  19080. if aType<>TypeInfo(TNullableCurrency) then
  19081. if aType<>TypeInfo(TNullableDateTime) then
  19082. if aType<>TypeInfo(TNullableTimeLog) then begin
  19083. result := sftUnknown;
  19084. exit;
  19085. end else
  19086. result := sftTimeLog else
  19087. result := sftDateTime else
  19088. result := sftCurrency else
  19089. result := sftFloat else
  19090. result := sftBoolean else
  19091. result := sftUTF8Text else
  19092. result := sftInteger else
  19093. result := sftUnknown;
  19094. end;
  19095. {$endif}
  19096. const
  19097. SQLFIELDTYPETODBFIELDTYPE: array[TSQLFieldType] of TSQLDBFieldType =
  19098. (ftUnknown, // sftUnknown
  19099. ftUTF8, // sftAnsiText
  19100. ftUTF8, // sftUTF8Text
  19101. ftInt64, // sftEnumerate
  19102. ftInt64, // sftSet
  19103. ftInt64, // sftInteger
  19104. ftInt64, // sftID = TSQLRecord(aID)
  19105. ftInt64, // sftRecord = TRecordReference = RecordRef
  19106. ftInt64, // sftBoolean
  19107. ftDouble, // sftFloat
  19108. ftDate, // sftDateTime
  19109. ftInt64, // sftTimeLog
  19110. ftCurrency, // sftCurrency
  19111. ftUTF8, // sftObject
  19112. {$ifndef NOVARIANTS}
  19113. ftUTF8, // sftVariant
  19114. ftNull, // sftNullable
  19115. {$endif}
  19116. ftBlob, // sftBlob
  19117. ftBlob, // sftBlobDynArray
  19118. ftBlob, // sftBlobCustom
  19119. ftUTF8, // sftUTF8Custom
  19120. ftUnknown, // sftMany
  19121. ftInt64, // sftModTime
  19122. ftInt64, // sftCreateTime
  19123. ftInt64, // sftTID
  19124. ftInt64, // sftRecordVersion = TRecordVersion
  19125. ftInt64); // sftSessionUserID
  19126. function SQLFieldTypeToDBField(aSQLFieldType: TSQLFieldType; aTypeInfo: pointer): TSQLDBFieldType;
  19127. {$ifdef HASINLINE}inline;{$endif}
  19128. begin
  19129. {$ifndef NOVARIANTS}
  19130. if aSQLFieldType=sftNullable then
  19131. result := SQLFIELDTYPETODBFIELDTYPE[NullableTypeToSQLFieldType(aTypeInfo)] else
  19132. {$endif}
  19133. result := SQLFIELDTYPETODBFIELDTYPE[aSqlFieldType];
  19134. end;
  19135. constructor TSQLPropInfo.Create(const aName: RawUTF8; aSQLFieldType: TSQLFieldType;
  19136. aAttributes: TSQLPropInfoAttributes; aFieldWidth, aPropertyIndex: integer);
  19137. begin
  19138. if aName='' then
  19139. EORMException.CreateUTF8('Void name for %.Create',[self]);
  19140. fName := aName;
  19141. fNameUnflattened := aName;
  19142. fSQLFieldType := aSQLFieldType;
  19143. fSQLFieldTypeStored := aSQLFieldType;
  19144. fSQLDBFieldType := SQLFIELDTYPETODBFIELDTYPE[fSQLFieldTypeStored];
  19145. fAttributes := aAttributes;
  19146. fFieldWidth := aFieldWidth;
  19147. fPropertyIndex := aPropertyIndex;
  19148. end;
  19149. function TSQLPropInfo.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  19150. var tmp: RawUTF8;
  19151. begin
  19152. GetValueVar(Instance,false,tmp,nil);
  19153. result := crc32c(0,pointer(tmp),length(tmp));
  19154. end;
  19155. procedure TSQLPropInfo.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  19156. var wasString: boolean;
  19157. tmp: RawUTF8;
  19158. begin
  19159. GetValueVar(Instance,false,tmp,@wasString);
  19160. if wasString then begin
  19161. W.Add('"');
  19162. if PtrUInt(tmp)<>0 then
  19163. W.AddJSONEscape(pointer(tmp),
  19164. {$ifdef FPC}length(tmp){$else}PInteger(PtrUInt(tmp)-4)^{$endif});
  19165. W.Add('"');
  19166. end else
  19167. if PtrUInt(tmp)<>0 then
  19168. W.AddNoJSONEscape(pointer(tmp),
  19169. {$ifdef FPC}length(tmp){$else}PInteger(PtrUInt(tmp)-4)^{$endif});
  19170. end;
  19171. function TSQLPropInfo.GetValue(Instance: TObject; ToSQL: boolean;
  19172. wasSQLString: PBoolean): RawUTF8;
  19173. begin
  19174. GetValueVar(Instance,ToSQL,Result,wasSQLString);
  19175. end;
  19176. procedure TSQLPropInfo.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean);
  19177. begin
  19178. SetValue(Instance,pointer(Value),wasString);
  19179. end;
  19180. function TSQLPropInfo.SQLDBFieldTypeName: PShortString;
  19181. begin
  19182. result := ToText(fSQLDBFieldType);
  19183. end;
  19184. procedure TSQLPropInfo.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  19185. var temp: RawByteString);
  19186. begin
  19187. GetValueVar(Instance,true,RawUTF8(temp),nil);
  19188. aValue.VType := fSQLDBFieldType;
  19189. case aValue.VType of
  19190. ftInt64:
  19191. SetInt64(pointer(temp),aValue.VInt64);
  19192. ftCurrency:
  19193. aValue.VInt64 := StrToCurr64(pointer(temp));
  19194. ftDouble:
  19195. aValue.VDouble := GetExtended(pointer(temp));
  19196. ftDate:
  19197. aValue.VDateTime := Iso8601ToDateTime(temp);
  19198. ftBlob: begin
  19199. temp := BlobToTSQLRawBlob(temp);
  19200. aValue.VBlob := pointer(temp);
  19201. aValue.VBlobLen := length(temp);
  19202. end;
  19203. ftUTF8:
  19204. aValue.VText := pointer(temp);
  19205. else
  19206. aValue.VInt64 := 0;
  19207. end;
  19208. end;
  19209. function TSQLPropInfo.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  19210. begin
  19211. case aValue.VType of
  19212. ftInt64:
  19213. SetValueVar(Instance,Int64ToUtf8(aValue.VInt64),false);
  19214. ftCurrency:
  19215. SetValueVar(Instance,Curr64ToStr(aValue.VInt64),false);
  19216. ftDouble:
  19217. SetValueVar(Instance,DoubleToStr(aValue.VDouble),false);
  19218. ftDate:
  19219. SetValueVar(Instance,DateTimeToIso8601Text(aValue.VDateTime),true);
  19220. ftBlob:
  19221. SetValueVar(Instance,TSQLRawBlobToBlob(aValue.VBlob,aValue.VBlobLen),true);
  19222. ftUTF8:
  19223. SetValue(Instance,aValue.VText,true);
  19224. else
  19225. SetValue(Instance,nil,false);
  19226. end;
  19227. result := true;
  19228. end;
  19229. const
  19230. NULL_LOW = ord('n')+ord('u')shl 8+ord('l')shl 16+ord('l')shl 24;
  19231. FALSE_LOW = ord('f')+ord('a')shl 8+ord('l')shl 16+ord('s')shl 24;
  19232. TRUE_LOW = ord('t')+ord('r')shl 8+ord('u')shl 16+ord('e')shl 24;
  19233. {$ifndef NOVARIANTS}
  19234. procedure ValueVarToVariant(Value: PUTF8Char; fieldType: TSQLFieldType;
  19235. var result: TVarData; createValueTempCopy: boolean; typeInfo: pointer;
  19236. options: TDocVariantOptions);
  19237. const
  19238. /// map our available types for any SQL field property into variant values
  19239. // - varNull will be used to store a true variant instance from JSON
  19240. SQL_ELEMENTTYPES: array[TSQLFieldType] of word = (
  19241. // sftUnknown, sftAnsiText, sftUTF8Text, sftEnumerate, sftSet, sftInteger,
  19242. varEmpty, varString, varString, varInteger, varInt64, varInt64,
  19243. // sftID, sftRecord, sftBoolean, sftFloat, sftDateTime, sftTimeLog, sftCurrency,
  19244. varInt64,varInt64,varBoolean, varDouble, varDate, varInt64, varCurrency,
  19245. //sftObject,{$NOVARIANTS}sftVariant,sftNullable{$endif} sftBlob,sftBlobDynArray,
  19246. varNull,{$ifndef NOVARIANTS} varNull, varNull, {$endif} varString, varNull,
  19247. // sftBlobCustom, sftUTF8Custom, sftMany, sftModTime, sftCreateTime, sftTID,
  19248. varString, varString, varEmpty, varInt64, varInt64, varInt64,
  19249. // sftRecordVersion, sftSessionUserID
  19250. varInt64, varInt64);
  19251. var tempCopy: RawByteString;
  19252. err: integer;
  19253. begin
  19254. if result.VType and VTYPE_STATIC<>0 then
  19255. VarClear(variant(result));
  19256. result.VType := SQL_ELEMENTTYPES[fieldType];
  19257. case fieldType of
  19258. sftCurrency:
  19259. result.VInt64 := StrToCurr64(Value);
  19260. sftFloat: begin
  19261. result.VDouble := GetExtended(Value,err);
  19262. if err<>0 then begin
  19263. result.VType := varString;
  19264. result.VAny := nil; // avoid GPF
  19265. RawUTF8(result.VAny) := Value;
  19266. end;
  19267. end;
  19268. sftDateTime:
  19269. result.VDate := Iso8601ToDateTimePUTF8Char(Value,0);
  19270. sftBoolean:
  19271. result.VBoolean :=
  19272. not((Value=nil) or (PWord(Value)^=ord('0')) or (PInteger(Value)^=FALSE_LOW));
  19273. sftEnumerate:
  19274. result.VInteger := GetInteger(Value);
  19275. sftInteger, sftID, sftTID, sftRecord, sftSet, sftRecordVersion, sftSessionUserID,
  19276. sftTimeLog, sftModTime, sftCreateTime:
  19277. SetInt64(Value,result.VInt64);
  19278. sftAnsiText, sftUTF8Text: begin
  19279. pointer(result.VAny) := nil;
  19280. RawUTF8(result.VAny) := Value;
  19281. end;
  19282. sftBlobCustom, sftBlob: begin
  19283. pointer(result.VAny) := nil;
  19284. RawByteString(result.VAny) := BlobToTSQLRawBlob(Value);
  19285. end;
  19286. sftBlobDynArray, sftObject, sftVariant, sftUTF8Custom, sftNullable: begin
  19287. if (fieldType=sftBlobDynArray) and (typeInfo<>nil) and
  19288. (Value<>nil) and (Value^<>'[') then begin
  19289. tempCopy := BlobToTSQLRawBlob(Value);
  19290. if tempCopy<>'' then begin
  19291. Value := pointer(DynArraySaveJSON(typeInfo,tempCopy));
  19292. createValueTempCopy := false;
  19293. end;
  19294. end;
  19295. if createValueTempCopy then begin
  19296. SetString(tempCopy,PAnsiChar(Value),StrLen(Value));
  19297. Value := pointer(tempCopy);
  19298. end;
  19299. GetVariantFromJSON(Value,false,variant(result),@options);
  19300. end;
  19301. end;
  19302. end;
  19303. function ObjectLoadVariant(var ObjectInstance; const aDocVariant: variant;
  19304. TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean;
  19305. var tmp: RawUTF8;
  19306. begin
  19307. if _Safe(aDocVariant)^.Kind<>dvObject then
  19308. result := false else begin
  19309. VariantSaveJSON(aDocVariant,twJSONEscape,tmp);
  19310. JSONToObject(ObjectInstance,pointer(tmp),result,TObjectListItemClass,Options);
  19311. end;
  19312. end;
  19313. procedure TSQLPropInfo.GetVariant(Instance: TObject; var Dest: Variant);
  19314. var temp: RawUTF8;
  19315. begin
  19316. GetValueVar(Instance,true,temp,nil);
  19317. ValueVarToVariant(pointer(temp),fSQLFieldTypeStored,TVarData(Dest),false,nil);
  19318. end;
  19319. procedure TSQLPropInfo.SetVariant(Instance: TObject; const Source: Variant);
  19320. begin
  19321. SetValueVar(Instance,VariantToUTF8(Source),TVarData(Source).VType and VTYPE_STATIC<>0);
  19322. end;
  19323. {$endif NOVARIANTS}
  19324. function TSQLPropInfo.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt;
  19325. var tmp1,tmp2: RawUTF8;
  19326. begin
  19327. if Item1=Item2 then
  19328. result := 0 else
  19329. if Item1=nil then
  19330. result := -1 else
  19331. if Item2=nil then
  19332. result := 1 else begin
  19333. GetValueVar(Item1,false,tmp1,nil);
  19334. GetValueVar(Item2,false,tmp2,nil);
  19335. if CaseInsensitive then // slow, always working implementation
  19336. result := StrIComp(pointer(tmp1),pointer(tmp2)) else
  19337. result := StrComp(pointer(tmp1),pointer(tmp2));
  19338. end;
  19339. end;
  19340. procedure TSQLPropInfo.CopyProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject);
  19341. procedure GenericCopy;
  19342. var tmp: RawUTF8;
  19343. wasString: boolean;
  19344. begin
  19345. GetValueVar(Source,false,tmp,@wasString);
  19346. DestInfo.SetValueVar(Dest,tmp,wasString);
  19347. end;
  19348. var i: integer;
  19349. begin
  19350. if (Source=nil) or (DestInfo=nil) or (Dest=nil) then
  19351. exit; // avoid GPF
  19352. with TSQLPropInfoRTTI(self) do
  19353. if fFromRTTI and (fFlattenedProps<>nil) then
  19354. for i := 0 to length(fFlattenedProps)-1 do
  19355. Source := fFlattenedProps[i].GetObjProp(Source);
  19356. with TSQLPropInfoRTTI(DestInfo) do
  19357. if fFromRTTI and (fFlattenedProps<>nil) then
  19358. for i := 0 to length(fFlattenedProps)-1 do
  19359. Dest := fFlattenedProps[i].GetObjProp(Dest);
  19360. if DestInfo.ClassType=ClassType then
  19361. CopySameClassProp(Source,DestInfo,Dest) else
  19362. GenericCopy;
  19363. end;
  19364. procedure TSQLPropInfo.CopyValue(Source, Dest: TObject);
  19365. begin
  19366. CopySameClassProp(Source,self,Dest);
  19367. end;
  19368. procedure TSQLPropInfo.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo;
  19369. Dest: TObject);
  19370. var tmp: RawUTF8;
  19371. wasString: boolean;
  19372. begin
  19373. GetValueVar(Source,false,tmp,@wasString);
  19374. DestInfo.SetValueVar(Dest,tmp,wasString);
  19375. end;
  19376. { TSQLPropInfoRTTI }
  19377. class function TSQLPropInfoRTTI.CreateFrom(aPropInfo: PPropInfo; aPropIndex: integer;
  19378. aOptions: TSQLPropInfoListOptions; const aFlattenedProps: PPropInfoDynArray): TSQLPropInfo;
  19379. var aSQLFieldType: TSQLFieldType;
  19380. aType: PTypeInfo;
  19381. C: TSQLPropInfoRTTIClass;
  19382. procedure FlattenedPropNameSet;
  19383. var i,max: Integer;
  19384. begin // Address.Street1 -> Address_Street1
  19385. (result as TSQLPropInfoRTTI).fFlattenedProps := aFlattenedProps;
  19386. result.fNameUnflattened := result.fName;
  19387. max := high(aFlattenedProps);
  19388. for i := max downto 0 do
  19389. result.fNameUnflattened := ToUTF8(aFlattenedProps[i]^.Name)+'.'+result.fNameUnflattened;
  19390. if (max>=0) and (aFlattenedProps[max]^.PropType^.
  19391. ClassFieldCount(pilIgnoreIfGetter in aOptions)=1) then begin
  19392. // Birth.Date -> Birth or Address.Country.Iso -> Address_Country
  19393. result.fName := ToUTF8(aFlattenedProps[max]^.Name);
  19394. dec(max);
  19395. end;
  19396. for i := max downto 0 do
  19397. result.fName := ToUTF8(aFlattenedProps[i]^.Name)+'_'+result.fName;
  19398. end;
  19399. begin
  19400. if aPropInfo=nil then
  19401. raise EORMException.CreateUTF8('Invalid %.CreateFrom(nil) call',[self]);
  19402. result := nil;
  19403. aSQLFieldType := sftUnknown;
  19404. aType := aPropInfo^.TypeInfo;
  19405. {$ifndef NOVARIANTS}
  19406. if aType^.Kind=tkVariant then begin
  19407. aSQLFieldType := NullableTypeToSQLFieldType(aType);
  19408. if aSQLFieldType<>sftUnknown then // handle sftNullable type
  19409. result := TSQLPropInfoRTTIVariant.Create(aPropInfo,aPropIndex,aSQLFieldType);
  19410. end;
  19411. {$endif}
  19412. if result=nil then begin
  19413. aSQLFieldType := aType^.GetSQLFieldType;
  19414. C := nil;
  19415. case aSQLFieldType of
  19416. sftUnknown, sftBlobCustom:
  19417. ; // will raise an EORMException
  19418. sftBoolean, sftEnumerate:
  19419. C := TSQLPropInfoRTTIEnum;
  19420. sftTimeLog, sftModTime, sftCreateTime: // specific class for further use
  19421. C := TSQLPropInfoRTTITimeLog;
  19422. sftCurrency:
  19423. C := TSQLPropInfoRTTICurrency;
  19424. sftDateTime:
  19425. C := TSQLPropInfoRTTIDateTime;
  19426. sftID: // = TSQLRecord(aID)
  19427. C := TSQLPropInfoRTTIID;
  19428. sftTID: // = TID or T*ID
  19429. C := TSQLPropInfoRTTITID;
  19430. sftSessionUserID:
  19431. C := TSQLPropInfoRTTIInt64;
  19432. sftRecord: // = TRecordReference/TRecordReferenceToBeDeleted
  19433. C := TSQLPropInfoRTTIRecordReference;
  19434. sftRecordVersion:
  19435. C := TSQLPropInfoRTTIRecordVersion;
  19436. sftMany:
  19437. C := TSQLPropInfoRTTIMany;
  19438. sftObject:
  19439. C := TSQLPropInfoRTTIObject;
  19440. {$ifndef NOVARIANTS}
  19441. sftVariant:
  19442. C := TSQLPropInfoRTTIVariant; // sftNullable already handle above
  19443. {$endif}
  19444. sftBlob:
  19445. C := TSQLPropInfoRTTIRawBlob;
  19446. sftBlobDynArray:
  19447. C := TSQLPropInfoRTTIDynArray;
  19448. sftUTF8Custom: // will happen only for DELPHI XE5 and up
  19449. result := TSQLPropInfoCustomJSON.Create(aPropInfo,aPropIndex);
  19450. else
  19451. case aType^.Kind of // retrieve exact type at binary level
  19452. tkInteger:
  19453. C := TSQLPropInfoRTTIInt32;
  19454. tkSet:
  19455. C := TSQLPropInfoRTTISet;
  19456. tkChar, tkWChar:
  19457. C := TSQLPropInfoRTTIChar;
  19458. tkInt64 {$ifdef FPC}, tkQWord{$endif}:
  19459. C := TSQLPropInfoRTTIInt64;
  19460. tkFloat:
  19461. if aType^.FloatType=ftDoub then
  19462. C := TSQLPropInfoRTTIDouble;
  19463. tkLString {$ifdef FPC},tkAString{$endif}:
  19464. case aType^.AnsiStringCodePage of // recognize optimized UTF-8/UTF-16
  19465. CP_UTF8: C := TSQLPropInfoRTTIRawUTF8;
  19466. CP_UTF16: C := TSQLPropInfoRTTIRawUnicode;
  19467. else C := TSQLPropInfoRTTIAnsi; // will use the right TSynAnsiConvert
  19468. end;
  19469. {$ifdef HASVARUSTRING}
  19470. tkUString:
  19471. C := TSQLPropInfoRTTIUnicode;
  19472. {$endif}
  19473. tkWString:
  19474. C := TSQLPropInfoRTTIWide;
  19475. end;
  19476. end;
  19477. if C<>nil then
  19478. result := C.Create(aPropInfo,aPropIndex,aSQLFieldType);
  19479. end;
  19480. if result<>nil then begin
  19481. if aFlattenedProps<>nil then
  19482. FlattenedPropNameSet;
  19483. end else
  19484. if pilRaiseEORMExceptionIfNotHandled in aOptions then
  19485. raise EORMException.CreateUTF8('%.CreateFrom: Unhandled %/% type for property %',
  19486. [self,ToText(aSQLFieldType)^,ToText(aType^.Kind)^,aPropInfo^.Name]);
  19487. end;
  19488. function TSQLPropInfoRTTI.GetSQLFieldRTTITypeName: RawUTF8;
  19489. begin
  19490. result := ToUTF8(fPropType^.Name);
  19491. end;
  19492. function TSQLPropInfoRTTI.GetFieldAddr(Instance: TObject): pointer;
  19493. begin
  19494. if Instance=nil then
  19495. result := nil else
  19496. result := fPropInfo^.GetFieldAddr(Instance);
  19497. end;
  19498. function TSQLPropInfoRTTI.Flattened(Instance: TObject): TObject;
  19499. var i: integer;
  19500. begin
  19501. result := Instance;
  19502. for i := 0 to length(fFlattenedProps)-1 do
  19503. result := fFlattenedProps[i].GetObjProp(result);
  19504. end;
  19505. {$ifndef NOVARIANTS}
  19506. procedure TSQLPropInfoRTTI.GetVariant(Instance: TObject; var Dest: Variant);
  19507. var temp: RawUTF8;
  19508. begin
  19509. GetValueVar(Instance,true,temp,nil);
  19510. ValueVarToVariant(pointer(temp),fSQLFieldTypeStored,TVarData(Dest),false,fPropInfo);
  19511. end;
  19512. {$endif NOVARIANTS}
  19513. constructor TSQLPropInfoRTTI.Create(aPropInfo: PPropInfo; aPropIndex: integer;
  19514. aSQLFieldType: TSQLFieldType);
  19515. var attrib: TSQLPropInfoAttributes;
  19516. begin
  19517. byte(attrib) := 0;
  19518. if aPropInfo^.IsStored(nil)=AS_UNIQUE then
  19519. Include(attrib,aIsUnique); // property MyProperty: RawUTF8 stored AS_UNIQUE;
  19520. inherited Create(ToUTF8(aPropInfo^.Name),aSQLFieldType,attrib,
  19521. aPropInfo^.Index,aPropIndex); // property MyProperty: RawUTF8 index 10; -> FieldWidth=10
  19522. fPropInfo := aPropInfo;
  19523. fPropType := aPropInfo^.TypeInfo;
  19524. if aPropInfo.GetterIsField then begin
  19525. fGetterIsFieldPropOffset := aPropInfo.GetProc{$ifndef FPC} and $00FFFFFF{$endif};
  19526. if (aPropInfo.SetProc=0) or (aPropInfo.SetProc=fPropInfo.GetProc) then
  19527. fInPlaceCopySameClassPropOffset := fGetterIsFieldPropOffset;
  19528. end;
  19529. fFromRTTI := true;
  19530. end;
  19531. { TSQLPropInfoRTTIInt32 }
  19532. procedure TSQLPropInfoRTTIInt32.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo;
  19533. Dest: TObject);
  19534. begin
  19535. TSQLPropInfoRTTIInt32(DestInfo).fPropInfo.SetOrdProp(Dest,fPropInfo.GetOrdProp(Source));
  19536. end;
  19537. procedure TSQLPropInfoRTTIInt32.GetBinary(Instance: TObject; W: TFileBufferWriter);
  19538. begin
  19539. W.WriteVarUInt32(cardinal(fPropInfo.GetOrdProp(Instance)));
  19540. end;
  19541. function TSQLPropInfoRTTIInt32.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  19542. begin
  19543. result := fPropInfo.GetOrdProp(Instance);
  19544. end;
  19545. procedure TSQLPropInfoRTTIInt32.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  19546. begin
  19547. W.Add(fPropInfo.GetOrdProp(Instance));
  19548. end;
  19549. procedure TSQLPropInfoRTTIInt32.GetValueVar(Instance: TObject;
  19550. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  19551. begin
  19552. if wasSQLString<>nil then
  19553. wasSQLString^ := false;
  19554. Int32ToUtf8(fPropInfo.GetOrdProp(Instance),result);
  19555. end;
  19556. procedure TSQLPropInfoRTTIInt32.NormalizeValue(var Value: RawUTF8);
  19557. var err, VInt: integer;
  19558. begin
  19559. VInt := GetInteger(pointer(Value),err);
  19560. if err<>0 then
  19561. Value := '' else
  19562. Int32ToUtf8(VInt,Value);
  19563. end;
  19564. function TSQLPropInfoRTTIInt32.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt;
  19565. begin
  19566. if Item1=Item2 then
  19567. result := 0 else
  19568. if Item1=nil then
  19569. result := -1 else
  19570. if Item2=nil then
  19571. result := 1 else
  19572. result := fPropInfo.GetOrdProp(Item1)-fPropInfo.GetOrdProp(Item2);
  19573. end;
  19574. function TSQLPropInfoRTTIInt32.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  19575. begin
  19576. if P<>nil then
  19577. fPropInfo.SetOrdProp(Instance,integer(FromVarUInt32(PByte(P))));
  19578. result := P;
  19579. end;
  19580. procedure TSQLPropInfoRTTIInt32.SetValue(Instance: TObject; Value: PUTF8Char;
  19581. wasString: boolean);
  19582. begin
  19583. fPropInfo.SetOrdProp(Instance,GetInteger(Value));
  19584. end;
  19585. function TSQLPropInfoRTTIInt32.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  19586. begin
  19587. if aValue.VType=ftInt64 then begin
  19588. fPropInfo.SetOrdProp(Instance,aValue.VInt64);
  19589. result := true;
  19590. end else
  19591. result := inherited SetFieldSQLVar(Instance,aValue);
  19592. end;
  19593. procedure TSQLPropInfoRTTIInt32.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  19594. var temp: RawByteString);
  19595. begin
  19596. aValue.VType := ftInt64;
  19597. aValue.VInt64 := fPropInfo.GetOrdProp(Instance);
  19598. end;
  19599. { TSQLPropInfoRTTISet }
  19600. constructor TSQLPropInfoRTTISet.Create(aPropInfo: PPropInfo; aPropIndex: integer;
  19601. aSQLFieldType: TSQLFieldType);
  19602. begin
  19603. inherited;
  19604. fSetEnumType := fPropType^.SetEnumType;
  19605. end;
  19606. { TSQLPropInfoRTTIEnum }
  19607. constructor TSQLPropInfoRTTIEnum.Create(aPropInfo: PPropInfo; aPropIndex: integer;
  19608. aSQLFieldType: TSQLFieldType);
  19609. begin
  19610. inherited;
  19611. fEnumType := fPropType^.EnumBaseType;
  19612. end;
  19613. procedure TSQLPropInfoRTTIEnum.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  19614. var i: integer;
  19615. begin
  19616. i := fPropInfo.GetOrdProp(Instance);
  19617. if fSQLFieldType=sftBoolean then
  19618. W.Add(i<>0) else
  19619. W.Add(i);
  19620. end;
  19621. function TSQLPropInfoRTTIEnum.GetCaption(Value: RawUTF8; out IntValue: integer): string;
  19622. begin
  19623. NormalizeValue(Value);
  19624. IntValue := GetInteger(pointer(Value));
  19625. if Value='' then
  19626. result := '' else
  19627. result := EnumType^.GetCaption(IntValue);
  19628. end;
  19629. procedure TSQLPropInfoRTTIEnum.GetValueVar(Instance: TObject;
  19630. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  19631. var i: integer;
  19632. begin
  19633. if wasSQLString<>nil then
  19634. wasSQLString^ := false;
  19635. i := fPropInfo.GetOrdProp(Instance);
  19636. if (fSQLFieldType=sftBoolean) and not ToSQL then
  19637. JSONBoolean(i<>0,result) else
  19638. Int32ToUtf8(i,result);
  19639. end;
  19640. procedure TSQLPropInfoRTTIEnum.NormalizeValue(var Value: RawUTF8);
  19641. var i,err: integer;
  19642. begin
  19643. i := GetInteger(pointer(Value),err);
  19644. if err<>0 then // we allow a value stated as text
  19645. if fSQLFieldType=sftBoolean then
  19646. i := Ord(IdemPropNameU(Value,'TRUE') or IdemPropNameU(Value,'YES')) else
  19647. i := fEnumType^.GetEnumNameValue(pointer(Value),length(Value)) else
  19648. if fSQLFieldType=sftBoolean then // normalize boolean values range to 0,1
  19649. if i<>0 then
  19650. i := 1;
  19651. if cardinal(i)>cardinal(fEnumType^.MaxValue) then
  19652. Value := '' else // only set a valid value
  19653. Int32ToUtf8(i,Value);
  19654. end;
  19655. procedure TSQLPropInfoRTTIEnum.SetValue(Instance: TObject; Value: PUTF8Char;
  19656. wasString: boolean);
  19657. var i,err,len: integer;
  19658. begin
  19659. if Value=nil then
  19660. i := 0 else begin
  19661. i := GetInteger(Value,err);
  19662. if err<>0 then begin // we allow a value stated as text
  19663. if fSQLFieldType=sftBoolean then begin
  19664. len := StrLen(Value);
  19665. i := Ord(IdemPropName('TRUE',Value,len) or IdemPropName('YES',Value,len));
  19666. end else
  19667. i := fEnumType^.GetEnumNameValue(Value); // -> convert into integer
  19668. if cardinal(i)>cardinal(fEnumType^.MaxValue) then
  19669. i := 0; // only set a valid text value
  19670. end else
  19671. if fSQLFieldType=sftBoolean then // normalize boolean values range to 0,1
  19672. if i<>0 then
  19673. i := 1;
  19674. end;
  19675. fPropInfo.SetOrdProp(Instance,i);
  19676. end;
  19677. { TSQLPropInfoRTTIChar }
  19678. procedure TSQLPropInfoRTTIChar.GetValueVar(Instance: TObject;
  19679. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  19680. var w: WideChar;
  19681. begin
  19682. w := WideChar(fPropInfo.GetOrdProp(Instance));
  19683. if ToSQL and (w=#0) then begin
  19684. // 'null' and not #0 to avoid end of SQL text - JSON will escape #0
  19685. result := 'null';
  19686. if wasSQLString<>nil then
  19687. wasSQLString^ := false;
  19688. end else begin
  19689. RawUnicodeToUtf8(@w,1,result);
  19690. if wasSQLString<>nil then
  19691. wasSQLString^ := true;
  19692. end;
  19693. end;
  19694. procedure TSQLPropInfoRTTIChar.NormalizeValue(var Value: RawUTF8);
  19695. begin // do nothing: should already be UTF-8 encoded
  19696. end;
  19697. procedure TSQLPropInfoRTTIChar.SetValue(Instance: TObject; Value: PUTF8Char;
  19698. wasString: boolean);
  19699. var i: integer;
  19700. begin
  19701. if (Value=nil) or (PInteger(Value)^=NULL_LOW) then
  19702. i := 0 else
  19703. i := GetUTF8Char(Value);
  19704. fPropInfo.SetOrdProp(Instance,i);
  19705. end;
  19706. { TSQLPropInfoRTTIInt64 }
  19707. procedure TSQLPropInfoRTTIInt64.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo;
  19708. Dest: TObject);
  19709. begin
  19710. TSQLPropInfoRTTIInt64(DestInfo).fPropInfo.SetInt64Prop(Dest,
  19711. fPropInfo.GetInt64Prop(Source));
  19712. end;
  19713. procedure TSQLPropInfoRTTIInt64.GetBinary(Instance: TObject;
  19714. W: TFileBufferWriter);
  19715. var V64: Int64;
  19716. begin
  19717. V64 := fPropInfo.GetInt64Prop(Instance);
  19718. W.Write(@V64,SizeOf(Int64));
  19719. end;
  19720. function TSQLPropInfoRTTIInt64.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  19721. var I64: Int64;
  19722. begin
  19723. if fPropInfo.GetterIsField then
  19724. I64 := PInt64(fPropInfo.GetterAddr(Instance))^ else
  19725. I64 := fPropInfo.GetInt64Prop(Instance);
  19726. result := Int64Rec(I64).Lo xor Int64Rec(I64).Hi;
  19727. end;
  19728. procedure TSQLPropInfoRTTIInt64.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  19729. begin
  19730. W.Add(fPropInfo.GetInt64Prop(Instance));
  19731. end;
  19732. procedure TSQLPropInfoRTTIInt64.GetValueVar(Instance: TObject;
  19733. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  19734. begin
  19735. if wasSQLString<>nil then
  19736. wasSQLString^ := false;
  19737. Int64ToUtf8(fPropInfo.GetInt64Prop(Instance),result);
  19738. end;
  19739. procedure TSQLPropInfoRTTIInt64.NormalizeValue(var Value: RawUTF8);
  19740. var err: integer;
  19741. VInt64: Int64;
  19742. begin
  19743. VInt64 := GetInt64(pointer(Value),err);
  19744. if err<>0 then
  19745. Value := '' else
  19746. Int64ToUtf8(VInt64,Value);
  19747. end;
  19748. function TSQLPropInfoRTTIInt64.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt;
  19749. var res64: Int64;
  19750. begin
  19751. if Item1=Item2 then
  19752. result := 0 else
  19753. if Item1=nil then
  19754. result := -1 else
  19755. if Item2=nil then
  19756. result := 1 else begin
  19757. if fGetterIsFieldPropOffset<>0 then
  19758. res64 := PInt64(PtrUInt(Item1)+fGetterIsFieldPropOffset)^-
  19759. PInt64(PtrUInt(Item2)+fGetterIsFieldPropOffset)^ else
  19760. res64 := fPropinfo.GetInt64Prop(Item1)-fPropinfo.GetInt64Prop(Item2);
  19761. if res64>0 then
  19762. result := 1 else
  19763. if res64<0 then
  19764. result := -1 else
  19765. result := 0;
  19766. end;
  19767. end;
  19768. function TSQLPropInfoRTTIInt64.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  19769. begin
  19770. if P=nil then
  19771. result := nil else begin
  19772. fPropInfo.SetInt64Prop(Instance,PInt64(P)^);
  19773. result := P+sizeof(Int64);
  19774. end;
  19775. end;
  19776. procedure TSQLPropInfoRTTIInt64.SetValue(Instance: TObject; Value: PUTF8Char;
  19777. wasString: boolean);
  19778. begin
  19779. fPropInfo.SetInt64Prop(Instance,GetInt64(Value));
  19780. end;
  19781. function TSQLPropInfoRTTIInt64.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  19782. begin
  19783. if aValue.VType=ftInt64 then begin
  19784. fPropInfo.SetInt64Prop(Instance,aValue.VInt64);
  19785. result := true;
  19786. end else
  19787. result := inherited SetFieldSQLVar(Instance,aValue);
  19788. end;
  19789. procedure TSQLPropInfoRTTIInt64.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  19790. var temp: RawByteString);
  19791. begin
  19792. aValue.VType := ftInt64;
  19793. aValue.VInt64 := fPropInfo.GetInt64Prop(Instance);
  19794. end;
  19795. { TSQLPropInfoRTTIDouble }
  19796. procedure TSQLPropInfoRTTIDouble.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo;
  19797. Dest: TObject);
  19798. begin
  19799. TSQLPropInfoRTTIDouble(DestInfo).fPropInfo.SetDoubleProp(Dest,
  19800. fPropInfo.GetDoubleProp(Source));
  19801. end;
  19802. procedure TSQLPropInfoRTTIDouble.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  19803. begin
  19804. W.AddDouble(fPropInfo.GetDoubleProp(Instance));
  19805. end;
  19806. procedure TSQLPropInfoRTTIDouble.GetValueVar(Instance: TObject;
  19807. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  19808. begin
  19809. if wasSQLString<>nil then
  19810. wasSQLString^ := false;
  19811. ExtendedToStr(fPropInfo.GetDoubleProp(Instance),DOUBLE_PRECISION,result);
  19812. end;
  19813. procedure TSQLPropInfoRTTIDouble.NormalizeValue(var Value: RawUTF8);
  19814. var VFloat: TSynExtended;
  19815. err: integer;
  19816. begin
  19817. VFloat := GetExtended(pointer(Value),err);
  19818. if err<>0 then
  19819. Value := '' else
  19820. ExtendedToStr(VFloat,DOUBLE_PRECISION,Value);
  19821. end;
  19822. procedure TSQLPropInfoRTTIDouble.SetValue(Instance: TObject; Value: PUTF8Char;
  19823. wasString: boolean);
  19824. var V: double;
  19825. err: integer;
  19826. begin
  19827. if Value=nil then
  19828. V := 0 else begin
  19829. V := GetExtended(pointer(Value),err);
  19830. if err<>0 then
  19831. V := 0;
  19832. end;
  19833. fPropInfo.SetDoubleProp(Instance,V);
  19834. end;
  19835. function TSQLPropInfoRTTIDouble.CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt;
  19836. var V1, V2: double;
  19837. begin
  19838. if Item1=Item2 then
  19839. result := 0 else
  19840. if Item1=nil then
  19841. result := -1 else
  19842. if Item2=nil then
  19843. result := 1 else begin
  19844. V1 := fPropInfo.GetDoubleProp(Item1);
  19845. V2 := fPropInfo.GetDoubleProp(Item2);
  19846. if SynCommons.SameValue(V1,V2) then
  19847. result := 0 else
  19848. if V1>V2 then
  19849. result := 1 else
  19850. result := -1;
  19851. end;
  19852. end;
  19853. procedure TSQLPropInfoRTTIDouble.GetBinary(Instance: TObject;
  19854. W: TFileBufferWriter);
  19855. var V: double;
  19856. begin
  19857. V := fPropInfo.GetDoubleProp(Instance);
  19858. W.Write(@V,SizeOf(V));
  19859. end;
  19860. {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}
  19861. type
  19862. unaligned = Double;
  19863. {$endif}
  19864. function TSQLPropInfoRTTIDouble.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  19865. begin
  19866. if P=nil then
  19867. result := nil else begin
  19868. fPropInfo.SetDoubleProp(Instance,unaligned(PDouble(P)^));
  19869. result := P+sizeof(double);
  19870. end;
  19871. end;
  19872. function TSQLPropInfoRTTIDouble.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  19873. var V: double;
  19874. begin
  19875. case aValue.VType of
  19876. ftCurrency: V := aValue.VCurrency;
  19877. ftDouble, ftDate: V := aValue.VDouble;
  19878. ftInt64: V := aValue.VInt64;
  19879. else begin
  19880. result := inherited SetFieldSQLVar(Instance,aValue);
  19881. exit;
  19882. end;
  19883. end;
  19884. fPropInfo.SetDoubleProp(Instance,V);
  19885. result := true;
  19886. end;
  19887. procedure TSQLPropInfoRTTIDouble.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  19888. var temp: RawByteString);
  19889. begin
  19890. aValue.VType := ftDouble;
  19891. aValue.VDouble := fPropInfo.GetDoubleProp(Instance);
  19892. end;
  19893. { TSQLPropInfoRTTICurrency }
  19894. procedure TSQLPropInfoRTTICurrency.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo;
  19895. Dest: TObject);
  19896. begin
  19897. TSQLPropInfoRTTICurrency(DestInfo).fPropInfo.SetCurrencyProp(Dest,
  19898. fPropInfo.GetCurrencyProp(Source));
  19899. end;
  19900. procedure TSQLPropInfoRTTICurrency.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  19901. begin
  19902. W.AddCurr64(fPropInfo.GetCurrencyProp(Instance));
  19903. end;
  19904. procedure TSQLPropInfoRTTICurrency.GetValueVar(Instance: TObject;
  19905. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  19906. begin
  19907. if wasSQLString<>nil then
  19908. wasSQLString^ := false;
  19909. result := CurrencyToStr(fPropInfo.GetCurrencyProp(Instance));
  19910. end;
  19911. procedure TSQLPropInfoRTTICurrency.NormalizeValue(var Value: RawUTF8);
  19912. begin
  19913. Value := Curr64ToStr(StrToCurr64(pointer(Value)));
  19914. end;
  19915. procedure TSQLPropInfoRTTICurrency.SetValue(Instance: TObject; Value: PUTF8Char;
  19916. wasString: boolean);
  19917. var tmp: Int64;
  19918. begin
  19919. tmp := StrToCurr64(Value,nil);
  19920. fPropInfo.SetCurrencyProp(Instance,PCurrency(@tmp)^);
  19921. end;
  19922. function TSQLPropInfoRTTICurrency.CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt;
  19923. var V1, V2: currency;
  19924. begin
  19925. if Item1=Item2 then
  19926. result := 0 else
  19927. if Item1=nil then
  19928. result := -1 else
  19929. if Item2=nil then
  19930. result := 1 else begin
  19931. V1 := fPropInfo.GetCurrencyProp(Item1);
  19932. V2 := fPropInfo.GetCurrencyProp(Item2);
  19933. Result := PInt64(@V1)^-PInt64(@V2)^;
  19934. end;
  19935. end;
  19936. procedure TSQLPropInfoRTTICurrency.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  19937. var temp: RawByteString);
  19938. begin
  19939. aValue.VType := ftCurrency;
  19940. aValue.VCurrency := fPropInfo.GetCurrencyProp(Instance);
  19941. end;
  19942. function TSQLPropInfoRTTICurrency.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  19943. var V: Currency;
  19944. begin
  19945. case aValue.VType of
  19946. ftDouble, ftDate: V := aValue.VDouble;
  19947. ftInt64: V := aValue.VInt64;
  19948. ftCurrency: V := aValue.VCurrency;
  19949. else begin
  19950. result := inherited SetFieldSQLVar(Instance,aValue);
  19951. exit;
  19952. end;
  19953. end;
  19954. fPropInfo.SetCurrencyProp(Instance,V);
  19955. result := true;
  19956. end;
  19957. procedure TSQLPropInfoRTTICurrency.GetBinary(Instance: TObject;
  19958. W: TFileBufferWriter);
  19959. var V: Currency;
  19960. begin
  19961. V := fPropInfo.GetCurrencyProp(Instance);
  19962. W.Write(@V,SizeOf(V));
  19963. end;
  19964. function TSQLPropInfoRTTICurrency.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  19965. begin
  19966. if P=nil then
  19967. result := nil else begin
  19968. fPropInfo.SetCurrencyProp(Instance,PCurrency(P)^);
  19969. result := P+sizeof(Currency);
  19970. end;
  19971. end;
  19972. { TSQLPropInfoRTTIDateTime }
  19973. procedure TSQLPropInfoRTTIDateTime.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  19974. begin
  19975. W.Add('"');
  19976. W.AddDateTime(fPropInfo.GetDoubleProp(Instance));
  19977. W.Add('"');
  19978. end;
  19979. function TSQLPropInfoRTTIDateTime.CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt;
  19980. var Date1,Date2: TTimeLogBits;
  19981. begin // force second resolution, as in our JSON content
  19982. if Item1=Item2 then
  19983. result := 0 else
  19984. if Item1=nil then
  19985. result := -1 else
  19986. if Item2=nil then
  19987. result := 1 else begin
  19988. Date1.From(fPropInfo.GetDoubleProp(Item1));
  19989. Date2.From(fPropInfo.GetDoubleProp(Item2));
  19990. result := Date1.Value-Date2.Value;
  19991. end;
  19992. end;
  19993. procedure TSQLPropInfoRTTIDateTime.GetValueVar(Instance: TObject;
  19994. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  19995. begin
  19996. if wasSQLString<>nil then
  19997. wasSQLString^ := true;
  19998. DateTimeToIso8601TextVar(fPropInfo.GetDoubleProp(Instance),'T',result);
  19999. end;
  20000. procedure TSQLPropInfoRTTIDateTime.NormalizeValue(var Value: RawUTF8);
  20001. begin
  20002. DateTimeToIso8601TextVar(Iso8601ToDateTime(Value),'T',Value);
  20003. end;
  20004. procedure TSQLPropInfoRTTIDateTime.SetValue(Instance: TObject; Value: PUTF8Char;
  20005. wasString: boolean);
  20006. var V: TDateTime;
  20007. begin
  20008. Iso8601ToDateTimePUTF8CharVar(Value,0,V);
  20009. fPropInfo.SetDoubleProp(Instance,V);
  20010. end;
  20011. procedure TSQLPropInfoRTTIDateTime.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  20012. var temp: RawByteString);
  20013. begin
  20014. aValue.VType := ftDate;
  20015. aValue.VDouble := fPropInfo.GetDoubleProp(Instance);
  20016. end;
  20017. { TSQLPropInfoRTTIMany }
  20018. // TSQLRecordMany stores nothing within the table
  20019. procedure TSQLPropInfoRTTIMany.GetValueVar(Instance: TObject;
  20020. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20021. begin
  20022. result := '';
  20023. end;
  20024. procedure TSQLPropInfoRTTIMany.SetValue(Instance: TObject; Value: PUTF8Char;
  20025. wasString: boolean);
  20026. begin
  20027. end;
  20028. procedure TSQLPropInfoRTTIMany.GetBinary(Instance: TObject; W: TFileBufferWriter);
  20029. begin
  20030. end;
  20031. function TSQLPropInfoRTTIMany.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  20032. begin
  20033. result := P;
  20034. end;
  20035. { TSQLPropInfoRTTIInstance }
  20036. constructor TSQLPropInfoRTTIInstance.Create(aPropInfo: PPropInfo; aPropIndex: integer;
  20037. aSQLFieldType: TSQLFieldType);
  20038. begin
  20039. inherited Create(aPropInfo,aPropIndex,aSQLFieldType);
  20040. fObjectClass := fPropType^.ClassType^.ClassType;
  20041. end;
  20042. function TSQLPropInfoRTTIInstance.GetInstance(Instance: TObject): TObject;
  20043. begin
  20044. result := fPropInfo.GetObjProp(Instance);
  20045. end;
  20046. procedure TSQLPropInfoRTTIInstance.SetInstance(Instance, Value: TObject);
  20047. begin
  20048. fPropInfo.SetOrdProp(Instance,PtrInt(Value));
  20049. end;
  20050. { TSQLPropInfoRTTIRecordReference }
  20051. constructor TSQLPropInfoRTTIRecordReference.Create(aPropInfo: PPropInfo;
  20052. aPropIndex: integer; aSQLFieldType: TSQLFieldType);
  20053. begin
  20054. inherited Create(aPropInfo,aPropIndex,aSQLFieldType);
  20055. fCascadeDelete := IdemPropName(fPropType^.Name,'TRecordReferenceToBeDeleted')
  20056. end;
  20057. { TSQLPropInfoRTTITID }
  20058. constructor TSQLPropInfoRTTITID.Create(aPropInfo: PPropInfo; aPropIndex: integer;
  20059. aSQLFieldType: TSQLFieldType);
  20060. var TypeName: PShortString;
  20061. ItemClass: TClass;
  20062. begin
  20063. inherited Create(aPropInfo,aPropIndex,aSQLFieldType);
  20064. TypeName := @fPropType^.Name;
  20065. if IdemPropName(TypeName^,'TID') or
  20066. (ord(TypeName^[1]) and $df<>ord('T')) or // expect T...ID pattern
  20067. (PWord(@TypeName^[ord(TypeName^[0])-1])^ and $dfdf<>ord('I')+ord('D') shl 8) or
  20068. (JSONSerializerRegisteredClass=nil) then
  20069. exit;
  20070. if (ord(TypeName^[0])>13) and
  20071. IdemPropName('ToBeDeletedID',@TypeName^[ord(TypeName^[0])-12],13) then begin
  20072. // 'TSQLRecordClientToBeDeletedID' -> TSQLRecordClient + CascadeDelete=true
  20073. fCascadeDelete := true;
  20074. ItemClass := JSONSerializerRegisteredClass.Find(@TypeName^[1],ord(TypeName^[0])-13);
  20075. end else
  20076. // 'TSQLRecordClientID' -> TSQLRecordClient
  20077. ItemClass := JSONSerializerRegisteredClass.Find(@TypeName^[1],ord(TypeName^[0])-2);
  20078. if (ItemClass<>nil) and ItemClass.InheritsFrom(TSQLRecord) then
  20079. fRecordClass := pointer(ItemClass);
  20080. end;
  20081. { TSQLPropInfoRTTIID }
  20082. procedure TSQLPropInfoRTTIID.SetValue(Instance: TObject; Value: PUTF8Char;
  20083. wasString: boolean);
  20084. begin
  20085. if TSQLRecord(Instance).fFill.JoinedFields then
  20086. raise EORMException.CreateUTF8('%(%).SetValue after Create*Joined',[self,Name]);
  20087. inherited SetValue(Instance,Value,wasString);
  20088. end;
  20089. procedure TSQLPropInfoRTTIID.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  20090. var ID: PtrUInt;
  20091. begin
  20092. ID := fPropInfo.GetOrdProp(Instance);
  20093. if TSQLRecord(Instance).fFill.JoinedFields then
  20094. ID := TSQLRecord(ID).fID;
  20095. W.AddU(ID);
  20096. end;
  20097. { TSQLPropInfoRTTIIObject }
  20098. procedure TSQLPropInfoRTTIObject.CopySameClassProp(Source: TObject;
  20099. DestInfo: TSQLPropInfo; Dest: TObject);
  20100. var S,D: TObject;
  20101. begin
  20102. // generic case: copy also class content (create instances)
  20103. S := GetInstance(Source);
  20104. D := TSQLPropInfoRTTIObject(DestInfo).GetInstance(Dest);
  20105. {$ifndef LVCL}
  20106. if S.InheritsFrom(TCollection) then
  20107. CopyCollection(TCollection(S),TCollection(D)) else
  20108. {$endif}
  20109. if S.InheritsFrom(TStrings) and D.InheritsFrom(TStrings) then
  20110. CopyStrings(TStrings(S),TStrings(D)) else begin
  20111. D.Free; // release previous instance
  20112. TSQLPropInfoRTTIObject(DestInfo).SetInstance(Dest,CopyObject(S));
  20113. end;
  20114. end;
  20115. procedure TSQLPropInfoRTTIObject.SetValue(Instance: TObject; Value: PUTF8Char;
  20116. wasString: boolean);
  20117. var valid: boolean;
  20118. ValueLocalCopy: RawUTF8;
  20119. begin
  20120. ValueLocalCopy := Value; // private copy since the buffer will be modified
  20121. PropInfo^.ClassFromJSON(Instance,pointer(ValueLocalCopy),valid);
  20122. end;
  20123. procedure TSQLPropInfoRTTIObject.GetValueVar(Instance: TObject;
  20124. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20125. begin
  20126. if wasSQLString<>nil then
  20127. wasSQLString^ := true;
  20128. result := ObjectToJSON(GetInstance(Instance));
  20129. end;
  20130. procedure TSQLPropInfoRTTIObject.GetBinary(Instance: TObject; W: TFileBufferWriter);
  20131. begin
  20132. // serialize object as JSON UTF-8 TEXT - not fast, but works
  20133. W.Write(ObjectToJSON(GetInstance(Instance)));
  20134. end;
  20135. function TSQLPropInfoRTTIObject.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  20136. var valid: boolean;
  20137. begin
  20138. // unserialize object from JSON UTF-8 TEXT - not fast, but works
  20139. PropInfo^.ClassFromJSON(Instance,pointer(FromVarString(PByte(P))),valid);
  20140. if valid then
  20141. result := P else
  20142. result := nil;
  20143. end;
  20144. function TSQLPropInfoRTTIObject.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20145. var tmp: RawUTF8;
  20146. begin // JSON is case-sensitive by design -> ignore CaseInsensitive parameter
  20147. tmp := ObjectToJSON(GetInstance(Instance));
  20148. result := crc32c(0,pointer(tmp),length(tmp));
  20149. end;
  20150. procedure TSQLPropInfoRTTIObject.NormalizeValue(var Value: RawUTF8);
  20151. begin // do nothing: should already be normalized
  20152. end;
  20153. procedure TSQLPropInfoRTTIObject.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  20154. begin
  20155. if jwoAsJsonNotAsString in W.fSQLRecordOptions then
  20156. W.WriteObject(GetInstance(Instance)) else
  20157. W.WriteObjectAsString(GetInstance(Instance));
  20158. end;
  20159. { TSQLPropInfoRTTIAnsi }
  20160. constructor TSQLPropInfoRTTIAnsi.Create(aPropInfo: PPropInfo; aPropIndex: integer;
  20161. aSQLFieldType: TSQLFieldType);
  20162. begin
  20163. inherited;
  20164. fEngine := TSynAnsiConvert.Engine(aPropInfo^.PropType^.AnsiStringCodePage);
  20165. end;
  20166. procedure TSQLPropInfoRTTIAnsi.CopySameClassProp(Source: TObject;
  20167. DestInfo: TSQLPropInfo; Dest: TObject);
  20168. var Value: RawByteString;
  20169. begin
  20170. if (TSQLPropInfoRTTIAnsi(DestInfo).fEngine=fEngine) then begin
  20171. fPropInfo.GetLongStrProp(Source,Value);
  20172. TSQLPropInfoRTTIAnsi(DestInfo).fPropInfo.SetLongStrProp(Dest,Value);
  20173. end else begin
  20174. GetValueVar(Source,false,RawUTF8(Value),nil);
  20175. DestInfo.SetValueVar(Dest,Value,true);
  20176. end;
  20177. end;
  20178. procedure TSQLPropInfoRTTIAnsi.GetBinary(Instance: TObject; W: TFileBufferWriter);
  20179. var Value: RawByteString;
  20180. begin
  20181. fPropInfo.GetLongStrProp(Instance,Value);
  20182. W.Write(Value);
  20183. end;
  20184. function TSQLPropInfoRTTIAnsi.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20185. var Up: array[byte] of AnsiChar; // avoid slow heap allocation
  20186. Value: RawByteString;
  20187. begin
  20188. fPropInfo.GetLongStrProp(Instance,Value);
  20189. if CaseInsensitive then
  20190. if fEngine.CodePage=CODEPAGE_US then
  20191. result := crc32c(0,Up,UpperCopyWin255(Up,Value)-Up) else
  20192. result := crc32c(0,Up,UpperCopy255Buf(Up,pointer(Value),length(Value))-Up) else
  20193. result := crc32c(0,pointer(Value),length(Value));
  20194. end;
  20195. procedure TSQLPropInfoRTTIAnsi.GetValueVar(Instance: TObject;
  20196. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20197. var tmp: RawByteString;
  20198. begin
  20199. if wasSQLString<>nil then
  20200. wasSQLString^ := true;
  20201. fPropInfo.GetLongStrProp(Instance,tmp);
  20202. result := fEngine.AnsiBufferToRawUTF8(pointer(tmp),length(tmp));
  20203. end;
  20204. procedure TSQLPropInfoRTTIAnsi.NormalizeValue(var Value: RawUTF8);
  20205. begin // do nothing: should already be UTF-8 encoded
  20206. end;
  20207. function TSQLPropInfoRTTIAnsi.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt;
  20208. var tmp1,tmp2: RawByteString;
  20209. begin
  20210. if Item1=Item2 then
  20211. result := 0 else
  20212. if Item1=nil then
  20213. result := -1 else
  20214. if Item2=nil then
  20215. result := 1 else begin
  20216. fPropInfo.GetLongStrProp(Item1,tmp1);
  20217. fPropInfo.GetLongStrProp(Item2,tmp2);
  20218. if CaseInsensitive then
  20219. if fEngine.CodePage=CODEPAGE_US then
  20220. result := AnsiIComp(pointer(tmp1),pointer(tmp2)) else
  20221. result := StrIComp(pointer(tmp1),pointer(tmp2)) else
  20222. result := StrComp(pointer(tmp1),pointer(tmp2));
  20223. end;
  20224. end;
  20225. function TSQLPropInfoRTTIAnsi.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  20226. var tmp: RawByteString;
  20227. begin
  20228. FromVarString(PByte(P),tmp,fEngine.CodePage);
  20229. fPropInfo.SetLongStrProp(Instance,tmp);
  20230. result := P;
  20231. end;
  20232. procedure TSQLPropInfoRTTIAnsi.SetValue(Instance: TObject; Value: PUTF8Char;
  20233. wasString: boolean);
  20234. begin
  20235. if Value=nil then
  20236. fPropInfo.SetLongStrProp(Instance,'') else
  20237. fPropInfo.SetLongStrProp(Instance,fEngine.UTF8BufferToAnsi(Value,StrLen(Value)));
  20238. end;
  20239. procedure TSQLPropInfoRTTIAnsi.SetValueVar(Instance: TObject; const Value: RawUTF8;
  20240. wasString: boolean);
  20241. begin
  20242. fPropInfo.SetLongStrProp(Instance,fEngine.UTF8ToAnsi(Value));
  20243. end;
  20244. procedure TSQLPropInfoRTTIAnsi.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  20245. var tmp: RawByteString;
  20246. begin
  20247. W.Add('"');
  20248. fPropInfo.GetLongStrProp(Instance,tmp);
  20249. if PtrUInt(tmp)<>0 then
  20250. W.AddAnyAnsiString(tmp,twJSONEscape,fEngine.CodePage);
  20251. W.Add('"');
  20252. end;
  20253. function TSQLPropInfoRTTIAnsi.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  20254. var tmp: RawByteString;
  20255. begin
  20256. case aValue.VType of
  20257. ftNull: ; // leave tmp=''
  20258. ftUTF8: fEngine.UTF8BufferToAnsi(aValue.VText,StrLen(aValue.VText),tmp);
  20259. else begin
  20260. result := inherited SetFieldSQLVar(Instance,aValue);
  20261. exit;
  20262. end;
  20263. end;
  20264. fPropInfo.SetLongStrProp(Instance,tmp);
  20265. result := True;
  20266. end;
  20267. procedure TSQLPropInfoRTTIAnsi.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  20268. var temp: RawByteString);
  20269. begin
  20270. fPropInfo.GetLongStrProp(Instance,temp);
  20271. temp := fEngine.AnsiToUTF8(temp);
  20272. aValue.VType := ftUTF8;
  20273. aValue.VText := pointer(temp);
  20274. end;
  20275. procedure TSQLPropInfoRTTIAnsi.CopyValue(Source, Dest: TObject);
  20276. begin // avoid temporary variable use, for simple fields with no getter/setter
  20277. if fInPlaceCopySameClassPropOffset=0 then
  20278. fPropInfo.CopyLongStrProp(Source,Dest) else
  20279. PRawByteString(PtrUInt(Dest)+fInPlaceCopySameClassPropOffset)^ :=
  20280. PRawByteString(PtrUInt(Source)+fInPlaceCopySameClassPropOffset)^;
  20281. end;
  20282. { TSQLPropInfoRTTIRawUTF8 }
  20283. procedure TSQLPropInfoRTTIRawUTF8.CopySameClassProp(Source: TObject;
  20284. DestInfo: TSQLPropInfo; Dest: TObject);
  20285. var Value: RawByteString;
  20286. begin
  20287. fPropInfo.GetLongStrProp(Source,Value);
  20288. TSQLPropInfoRTTIRawUTF8(DestInfo).fPropInfo.SetLongStrProp(Dest,Value);
  20289. end;
  20290. function TSQLPropInfoRTTIRawUTF8.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20291. var Up: array[byte] of AnsiChar; // avoid slow heap allocation
  20292. Value: RawByteString;
  20293. begin
  20294. fPropInfo.GetLongStrProp(Instance,Value);
  20295. if CaseInsensitive then
  20296. result := crc32c(0,Up,UTF8UpperCopy255(Up,Value)-Up) else
  20297. result := crc32c(0,pointer(Value),length(Value));
  20298. end;
  20299. procedure TSQLPropInfoRTTIRawUTF8.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  20300. var tmp: RawByteString;
  20301. begin
  20302. W.Add('"');
  20303. fPropInfo.GetLongStrProp(Instance,tmp);
  20304. if PtrUInt(tmp)<>0 then
  20305. W.AddJSONEscape(pointer(tmp),
  20306. {$ifdef FPC}length(tmp){$else}PInteger(PtrUInt(tmp)-4)^{$endif});
  20307. W.Add('"');
  20308. end;
  20309. procedure TSQLPropInfoRTTIRawUTF8.GetValueVar(Instance: TObject;
  20310. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20311. begin
  20312. if wasSQLString<>nil then
  20313. wasSQLString^ := true;
  20314. fPropInfo.GetLongStrProp(Instance,RawByteString(result));
  20315. end;
  20316. function TSQLPropInfoRTTIRawUTF8.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  20317. var tmp: RawByteString;
  20318. begin
  20319. case aValue.VType of
  20320. ftNull: ; // leave tmp=''
  20321. ftUTF8: SetString(tmp,PAnsiChar(aValue.VText),StrLen(aValue.VText));
  20322. else begin
  20323. result := inherited SetFieldSQLVar(Instance,aValue);
  20324. exit;
  20325. end;
  20326. end;
  20327. fPropInfo.SetLongStrProp(Instance,tmp);
  20328. result := True;
  20329. end;
  20330. procedure TSQLPropInfoRTTIRawUTF8.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  20331. var temp: RawByteString);
  20332. begin
  20333. fPropInfo.GetLongStrProp(Instance,temp);
  20334. aValue.VType := ftUTF8;
  20335. aValue.VText := Pointer(temp);
  20336. end;
  20337. function TSQLPropInfoRTTIRawUTF8.CompareValue(Item1, Item2: TObject;
  20338. CaseInsensitive: boolean): PtrInt;
  20339. procedure Generic;
  20340. var tmp1,tmp2: RawByteString;
  20341. begin
  20342. fPropInfo.GetLongStrProp(Item1,tmp1);
  20343. fPropInfo.GetLongStrProp(Item2,tmp2);
  20344. if CaseInsensitive then
  20345. result := UTF8IComp(pointer(tmp1),pointer(tmp2)) else
  20346. result := StrComp(pointer(tmp1),pointer(tmp2));
  20347. end;
  20348. begin
  20349. if Item1=Item2 then
  20350. result := 0 else
  20351. if Item1=nil then
  20352. result := -1 else
  20353. if Item2=nil then
  20354. result := 1 else
  20355. if fGetterIsFieldPropOffset<>0 then // avoid any temporary variable
  20356. if CaseInsensitive then
  20357. result := UTF8IComp(PPointer(PtrUInt(Item1)+fGetterIsFieldPropOffset)^,
  20358. PPointer(PtrUInt(Item2)+fGetterIsFieldPropOffset)^) else
  20359. result := StrComp(PPointer(PtrUInt(Item1)+fGetterIsFieldPropOffset)^,
  20360. PPointer(PtrUInt(Item2)+fGetterIsFieldPropOffset)^) else
  20361. Generic;
  20362. end;
  20363. procedure TSQLPropInfoRTTIRawUTF8.SetValue(Instance: TObject; Value: PUTF8Char;
  20364. wasString: boolean);
  20365. var tmp: RawUTF8;
  20366. begin
  20367. if Value<>nil then
  20368. SetString(tmp,PAnsiChar(Value),StrLen(Value));
  20369. fPropInfo.SetLongStrProp(Instance,tmp);
  20370. end;
  20371. procedure TSQLPropInfoRTTIRawUTF8.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean);
  20372. begin
  20373. fPropInfo.SetLongStrProp(Instance,Value);
  20374. end;
  20375. { TSQLPropInfoRTTIRawUnicode }
  20376. procedure TSQLPropInfoRTTIRawUnicode.CopySameClassProp(Source: TObject;
  20377. DestInfo: TSQLPropInfo; Dest: TObject);
  20378. var Value: RawByteString;
  20379. begin
  20380. fPropInfo.GetLongStrProp(Source,Value);
  20381. TSQLPropInfoRTTIRawUnicode(DestInfo).fPropInfo.SetLongStrProp(Dest,Value);
  20382. end;
  20383. function TSQLPropInfoRTTIRawUnicode.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20384. var Up: array[byte] of AnsiChar; // avoid slow heap allocation
  20385. Value: RawByteString;
  20386. begin
  20387. fPropInfo.GetLongStrProp(Instance,Value);
  20388. if CaseInsensitive then
  20389. result := crc32c(0,Up,UpperCopy255W(Up,pointer(Value),length(Value)shr 1)-Up) else
  20390. result := crc32c(0,pointer(Value),length(Value));
  20391. end;
  20392. procedure TSQLPropInfoRTTIRawUnicode.GetValueVar(Instance: TObject;
  20393. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20394. var tmp: RawByteString;
  20395. begin
  20396. if wasSQLString<>nil then
  20397. wasSQLString^ := true;
  20398. fPropInfo.GetLongStrProp(Instance,tmp);
  20399. RawUnicodeToUTF8(pointer(tmp),length(tmp)shr 1,result);
  20400. end;
  20401. function TSQLPropInfoRTTIRawUnicode.CompareValue(Item1, Item2: TObject;
  20402. CaseInsensitive: boolean): PtrInt;
  20403. var tmp1,tmp2: RawByteString;
  20404. begin
  20405. if Item1=Item2 then
  20406. result := 0 else
  20407. if Item1=nil then
  20408. result := -1 else
  20409. if Item2=nil then
  20410. result := 1 else begin
  20411. fPropInfo.GetLongStrProp(Item1,tmp1);
  20412. fPropInfo.GetLongStrProp(Item2,tmp2);
  20413. if CaseInsensitive then
  20414. result := AnsiICompW(pointer(tmp1),pointer(tmp2)) else
  20415. result := StrCompW(pointer(tmp1),pointer(tmp2));
  20416. end;
  20417. end;
  20418. procedure TSQLPropInfoRTTIRawUnicode.SetValue(Instance: TObject; Value: PUTF8Char;
  20419. wasString: boolean);
  20420. begin
  20421. if Value=nil then
  20422. fPropInfo.SetLongStrProp(Instance,'') else
  20423. fPropInfo.SetLongStrProp(Instance,Utf8DecodeToRawUnicode(Value,StrLen(Value)));
  20424. end;
  20425. procedure TSQLPropInfoRTTIRawUnicode.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean);
  20426. begin
  20427. fPropInfo.SetLongStrProp(Instance,Utf8DecodeToRawUnicode(Value));
  20428. end;
  20429. { TSQLPropInfoRTTIRawBlob }
  20430. procedure TSQLPropInfoRTTIRawBlob.CopySameClassProp(Source: TObject;
  20431. DestInfo: TSQLPropInfo; Dest: TObject);
  20432. var Value: RawByteString;
  20433. begin
  20434. fPropInfo.GetLongStrProp(Source,Value);
  20435. TSQLPropInfoRTTIRawBlob(DestInfo).fPropInfo.SetLongStrProp(Dest,Value);
  20436. end;
  20437. function TSQLPropInfoRTTIRawBlob.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20438. var Value: RawByteString;
  20439. begin
  20440. fPropInfo.GetLongStrProp(Instance,Value);
  20441. result := crc32c(0,pointer(Value),length(Value)); // binary -> case sensitive
  20442. end;
  20443. procedure TSQLPropInfoRTTIRawBlob.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  20444. var tmp: RawByteString;
  20445. begin
  20446. fPropInfo.GetLongStrProp(Instance,tmp);
  20447. W.WrBase64(pointer(tmp),length(tmp),true);
  20448. end;
  20449. procedure TSQLPropInfoRTTIRawBlob.GetBlob(Instance: TObject;
  20450. var Blob: RawByteString);
  20451. begin
  20452. fPropInfo.GetLongStrProp(Instance,Blob);
  20453. end;
  20454. procedure TSQLPropInfoRTTIRawBlob.SetBlob(Instance: TObject;
  20455. const Blob: RawByteString);
  20456. begin
  20457. fPropInfo.SetLongStrProp(Instance,Blob);
  20458. end;
  20459. function TSQLPropInfoRTTIRawBlob.IsNull(Instance: TObject): Boolean;
  20460. var Blob: RawByteString;
  20461. begin
  20462. fPropInfo.GetLongStrProp(Instance,Blob);
  20463. result := (Blob='');
  20464. end;
  20465. procedure TSQLPropInfoRTTIRawBlob.GetValueVar(Instance: TObject;
  20466. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20467. begin
  20468. fPropInfo.GetLongStrProp(Instance,RawByteString(result));
  20469. BinaryToText(result,ToSQL,wasSQLString);
  20470. end;
  20471. function TSQLPropInfoRTTIRawBlob.CompareValue(Item1, Item2: TObject;
  20472. CaseInsensitive: boolean): PtrInt;
  20473. var tmp1,tmp2: RawByteString;
  20474. begin
  20475. if Item1=Item2 then
  20476. result := 0 else
  20477. if Item1=nil then
  20478. result := -1 else
  20479. if Item2=nil then
  20480. result := 1 else begin
  20481. fPropInfo.GetLongStrProp(Item1,tmp1);
  20482. fPropInfo.GetLongStrProp(Item2,tmp2);
  20483. // BLOB is binary so always case sensitive
  20484. result := StrComp(pointer(tmp1),pointer(tmp2));
  20485. end;
  20486. end;
  20487. procedure TSQLPropInfoRTTIRawBlob.SetValue(Instance: TObject; Value: PUTF8Char;
  20488. wasString: boolean);
  20489. begin
  20490. fPropInfo.SetLongStrProp(Instance,BlobToTSQLRawBlob(Value));
  20491. end;
  20492. procedure TSQLPropInfoRTTIRawBlob.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean);
  20493. begin
  20494. fPropInfo.SetLongStrProp(Instance,BlobToTSQLRawBlob(Value));
  20495. end;
  20496. function TSQLPropInfoRTTIRawBlob.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  20497. var tmp: RawByteString;
  20498. begin
  20499. case aValue.VType of
  20500. ftBlob: begin
  20501. SetString(tmp,PAnsiChar(aValue.VBlob),aValue.VBlobLen);
  20502. fPropInfo.SetLongStrProp(Instance,tmp);
  20503. result := true;
  20504. end;
  20505. ftNull: begin
  20506. fPropInfo.SetLongStrProp(Instance,'');
  20507. result := true;
  20508. end;
  20509. else result := inherited SetFieldSQLVar(Instance,aValue);
  20510. end;
  20511. end;
  20512. procedure TSQLPropInfoRTTIRawBlob.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  20513. var temp: RawByteString);
  20514. begin
  20515. fPropInfo.GetLongStrProp(Instance,temp);
  20516. if temp='' then
  20517. aValue.VType := ftNull else begin
  20518. aValue.VType := ftBlob;
  20519. aValue.VBlob := pointer(temp);
  20520. aValue.VBlobLen := length(temp);
  20521. end;
  20522. end;
  20523. { TSQLPropInfoRTTIWide }
  20524. procedure TSQLPropInfoRTTIWide.CopySameClassProp(Source: TObject;
  20525. DestInfo: TSQLPropInfo; Dest: TObject);
  20526. var Value: WideString;
  20527. begin
  20528. fPropInfo.GetWideStrProp(Source,Value);
  20529. TSQLPropInfoRTTIWide(DestInfo).fPropInfo.SetWideStrProp(Dest,Value);
  20530. end;
  20531. procedure TSQLPropInfoRTTIWide.GetBinary(Instance: TObject; W: TFileBufferWriter);
  20532. var Value: WideString;
  20533. begin
  20534. fPropInfo.GetWideStrProp(Instance,Value);
  20535. W.Write(WideStringToUTF8(Value));
  20536. end;
  20537. function TSQLPropInfoRTTIWide.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20538. var Up: array[byte] of AnsiChar; // avoid slow heap allocation
  20539. Value: WideString;
  20540. begin
  20541. fPropInfo.GetWideStrProp(Instance,Value);
  20542. if CaseInsensitive then
  20543. result := crc32c(0,Up,UpperCopy255W(Up,pointer(Value),length(Value))-Up) else
  20544. result := crc32c(0,pointer(Value),length(Value)*2);
  20545. end;
  20546. procedure TSQLPropInfoRTTIWide.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  20547. var Value: WideString;
  20548. begin
  20549. W.Add('"');
  20550. fPropInfo.GetWideStrProp(Instance,Value);
  20551. if pointer(Value)<>nil then
  20552. W.AddJSONEscapeW(pointer(Value),0);
  20553. W.Add('"');
  20554. end;
  20555. procedure TSQLPropInfoRTTIWide.GetValueVar(Instance: TObject;
  20556. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20557. var Value: WideString;
  20558. begin
  20559. fPropInfo.GetWideStrProp(Instance,Value);
  20560. result := WideStringToUTF8(Value);
  20561. if wasSQLString<>nil then
  20562. wasSQLString^ := true;
  20563. end;
  20564. procedure TSQLPropInfoRTTIWide.CopyValue(Source, Dest: TObject);
  20565. begin // avoid temporary variable use, for simple fields with no getter/setter
  20566. if fInPlaceCopySameClassPropOffset=0 then
  20567. CopySameClassProp(Source,self,Dest) else
  20568. PWideString(PtrUInt(Dest)+fInPlaceCopySameClassPropOffset)^ :=
  20569. PWideString(PtrUInt(Source)+fInPlaceCopySameClassPropOffset)^;
  20570. end;
  20571. function TSQLPropInfoRTTIWide.CompareValue(Item1, Item2: TObject;
  20572. CaseInsensitive: boolean): PtrInt;
  20573. var tmp1,tmp2: WideString;
  20574. begin
  20575. if Item1=Item2 then
  20576. result := 0 else
  20577. if Item1=nil then
  20578. result := -1 else
  20579. if Item2=nil then
  20580. result := 1 else begin
  20581. fPropInfo.GetWideStrProp(Item1,tmp1);
  20582. fPropInfo.GetWideStrProp(Item2,tmp2);
  20583. if CaseInsensitive then
  20584. result := AnsiICompW(pointer(tmp1),pointer(tmp2)) else
  20585. result := StrCompW(pointer(tmp1),pointer(tmp2));
  20586. end;
  20587. end;
  20588. function TSQLPropInfoRTTIWide.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  20589. begin
  20590. fPropInfo.SetWideStrProp(Instance,UTF8ToWideString(FromVarString(PByte(P))));
  20591. result := P;
  20592. end;
  20593. procedure TSQLPropInfoRTTIWide.SetValue(Instance: TObject; Value: PUTF8Char;
  20594. wasString: boolean);
  20595. var Wide: WideString;
  20596. begin
  20597. if Value<>nil then
  20598. UTF8ToWideString(Value,StrLen(Value),Wide);
  20599. fPropInfo.SetWideStrProp(Instance,Wide);
  20600. end;
  20601. {$ifdef HASVARUSTRING}
  20602. { TSQLPropInfoRTTIUnicode }
  20603. procedure TSQLPropInfoRTTIUnicode.CopySameClassProp(Source: TObject;
  20604. DestInfo: TSQLPropInfo; Dest: TObject);
  20605. begin
  20606. TSQLPropInfoRTTIUnicode(DestInfo).fPropInfo.SetUnicodeStrProp(Dest,
  20607. fPropInfo.GetUnicodeStrProp(Source));
  20608. end;
  20609. procedure TSQLPropInfoRTTIUnicode.GetBinary(Instance: TObject; W: TFileBufferWriter);
  20610. begin
  20611. W.Write(UnicodeStringToUtf8(fPropInfo.GetUnicodeStrProp(Instance)));
  20612. end;
  20613. procedure TSQLPropInfoRTTIUnicode.CopyValue(Source, Dest: TObject);
  20614. begin // avoid temporary variable use, for simple fields with no getter/setter
  20615. if fInPlaceCopySameClassPropOffset=0 then
  20616. CopySameClassProp(Source,self,Dest) else
  20617. PUnicodeString(PtrUInt(Dest)+fInPlaceCopySameClassPropOffset)^ :=
  20618. PUnicodeString(PtrUInt(Source)+fInPlaceCopySameClassPropOffset)^;
  20619. end;
  20620. function TSQLPropInfoRTTIUnicode.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20621. var Up: array[byte] of AnsiChar; // avoid slow heap allocation
  20622. Value: UnicodeString;
  20623. begin
  20624. Value := fPropInfo.GetUnicodeStrProp(Instance);
  20625. if CaseInsensitive then
  20626. result := crc32c(0,Up,UpperCopy255W(Up,pointer(Value),length(Value))-Up) else
  20627. result := crc32c(0,pointer(Value),length(Value)*2);
  20628. end;
  20629. procedure TSQLPropInfoRTTIUnicode.GetValueVar(Instance: TObject;
  20630. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20631. begin
  20632. result := UnicodeStringToUtf8(fPropInfo.GetUnicodeStrProp(Instance));
  20633. if wasSQLString<>nil then
  20634. wasSQLString^ := true;
  20635. end;
  20636. procedure TSQLPropInfoRTTIUnicode.GetJSONValues(Instance: TObject; W: TJSONSerializer);
  20637. var tmp: UnicodeString;
  20638. begin
  20639. W.Add('"');
  20640. tmp := fPropInfo.GetUnicodeStrProp(Instance);
  20641. if PtrUInt(tmp)<>0 then
  20642. W.AddJSONEscapeW(pointer(tmp),0);
  20643. W.Add('"');
  20644. end;
  20645. function TSQLPropInfoRTTIUnicode.CompareValue(Item1, Item2: TObject;
  20646. CaseInsensitive: boolean): PtrInt;
  20647. var tmp1,tmp2: UnicodeString;
  20648. begin
  20649. if Item1=Item2 then
  20650. result := 0 else
  20651. if Item1=nil then
  20652. result := -1 else
  20653. if Item2=nil then
  20654. result := 1 else begin
  20655. tmp1 := fPropInfo.GetUnicodeStrProp(Item1);
  20656. tmp2 := fPropInfo.GetUnicodeStrProp(Item2);
  20657. if CaseInsensitive then
  20658. result := AnsiICompW(pointer(tmp1),pointer(tmp2)) else
  20659. result := StrCompW(pointer(tmp1),pointer(tmp2));
  20660. end;
  20661. end;
  20662. function TSQLPropInfoRTTIUnicode.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  20663. begin
  20664. fPropInfo.SetUnicodeStrProp(Instance,UTF8DecodeToUnicodeString(FromVarString(PByte(P))));
  20665. result := P;
  20666. end;
  20667. procedure TSQLPropInfoRTTIUnicode.SetValue(Instance: TObject; Value: PUTF8Char;
  20668. wasString: boolean);
  20669. var tmp: UnicodeString;
  20670. begin
  20671. if Value<>nil then
  20672. UTF8DecodeToUnicodeString(Value,StrLen(Value),tmp);
  20673. fPropInfo.SetUnicodeStrProp(Instance,tmp);
  20674. end;
  20675. procedure TSQLPropInfoRTTIUnicode.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean);
  20676. begin
  20677. fPropInfo.SetUnicodeStrProp(Instance,UTF8DecodeToUnicodeString(Value));
  20678. end;
  20679. function TSQLPropInfoRTTIUnicode.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  20680. var tmp: UnicodeString;
  20681. begin
  20682. case aValue.VType of
  20683. ftNull: ; // leave tmp=''
  20684. ftUTF8: UTF8DecodeToUnicodeString(aValue.VText,StrLen(aValue.VText),tmp);
  20685. else begin
  20686. result := inherited SetFieldSQLVar(Instance,aValue);
  20687. exit;
  20688. end;
  20689. end;
  20690. fPropInfo.SetUnicodeStrProp(Instance,tmp);
  20691. result := True;
  20692. end;
  20693. procedure TSQLPropInfoRTTIUnicode.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  20694. var temp: RawByteString);
  20695. begin
  20696. temp := UnicodeStringToUtf8(fPropInfo.GetUnicodeStrProp(Instance));
  20697. aValue.VType := ftUTF8;
  20698. aValue.VText := Pointer(temp);
  20699. end;
  20700. {$endif HASVARUSTRING}
  20701. { TObjArraySerializer}
  20702. type
  20703. TObjArraySerializer = class(TPointerClassHashed)
  20704. public
  20705. Instance: TClassInstance;
  20706. procedure CustomWriter(const aWriter: TTextWriter; const aValue);
  20707. function CustomReader(P: PUTF8Char; var aValue; out aValid: Boolean): PUTF8Char;
  20708. end;
  20709. PTObjArraySerializer = ^TObjArraySerializer;
  20710. procedure TObjArraySerializer.CustomWriter(const aWriter: TTextWriter; const aValue);
  20711. var options: TTextWriterWriteObjectOptions;
  20712. begin
  20713. if twoEnumSetsAsTextInRecord in aWriter.CustomOptions then
  20714. options := [woDontStoreDefault,woSQLRawBlobAsBase64,woEnumSetsAsText] else
  20715. options := [woDontStoreDefault,woSQLRawBlobAsBase64];
  20716. aWriter.WriteObject(TObject(aValue), options);
  20717. end;
  20718. function TObjArraySerializer.CustomReader(P: PUTF8Char; var aValue;
  20719. out aValid: Boolean): PUTF8Char;
  20720. begin
  20721. if TObject(aValue)=nil then
  20722. TObject(aValue) := Instance.CreateNew;
  20723. result := JSONToObject(aValue,P,aValid,nil,JSONTOOBJECT_TOLERANTOPTIONS);
  20724. end;
  20725. function InternalIsObjArray(aDynArrayTypeInfo: pointer): boolean;
  20726. begin
  20727. result := ObjArraySerializers.Find(aDynArrayTypeInfo)<>nil;
  20728. end;
  20729. { TSQLPropInfoRTTIDynArray }
  20730. constructor TSQLPropInfoRTTIDynArray.Create(aPropInfo: PPropInfo;
  20731. aPropIndex: integer; aSQLFieldType: TSQLFieldType);
  20732. begin
  20733. inherited Create(aPropInfo,aPropIndex,aSQLFieldType);
  20734. fObjArray := aPropInfo^.DynArrayIsObjArrayInstance;
  20735. if fGetterIsFieldPropOffset=0 then
  20736. raise EORMException.CreateUTF8('%.Create(%) getter!',[self,fPropType^.Name]);
  20737. end;
  20738. function TSQLPropInfoRTTIDynArray.GetDynArray(Instance: TObject): TDynArray;
  20739. begin
  20740. GetDynArray(Instance,result);
  20741. end;
  20742. procedure TSQLPropInfoRTTIDynArray.GetDynArray(Instance: TObject; var result: TDynArray);
  20743. begin
  20744. result.Init(fPropType,pointer(PtrUInt(Instance)+fGetterIsFieldPropOffset)^);
  20745. result.IsObjArray := fObjArray<>nil; // no need to search
  20746. end;
  20747. procedure TSQLPropInfoRTTIDynArray.Serialize(Instance: TObject;
  20748. var data: RawByteString; ExtendedJson: boolean);
  20749. var da: TDynArray;
  20750. begin
  20751. GetDynArray(Instance,da);
  20752. if fObjArray<>nil then
  20753. with TJSONSerializer.CreateOwnedStream(8192) do
  20754. try
  20755. if ExtendedJson then
  20756. include(fCustomOptions,twoForceJSONExtended); // smaller content
  20757. AddDynArrayJSON(da);
  20758. SetText(RawUTF8(data));
  20759. finally
  20760. Free;
  20761. end else
  20762. data := da.SaveTo;
  20763. end;
  20764. procedure TSQLPropInfoRTTIDynArray.CopySameClassProp(Source: TObject;
  20765. DestInfo: TSQLPropInfo; Dest: TObject);
  20766. var SourceArray,DestArray: TDynArray;
  20767. begin
  20768. GetDynArray(Source,SourceArray);
  20769. TSQLPropInfoRTTIDynArray(DestInfo).GetDynArray(Dest,DestArray);
  20770. if (fObjArray<>nil) or (TSQLPropInfoRTTIDynArray(DestInfo).fObjArray<>nil) or
  20771. (SourceArray.ArrayType<>DestArray.ArrayType) then
  20772. DestArray.LoadFromJSON(pointer(SourceArray.SaveToJSON)) else
  20773. DestArray.Copy(SourceArray);
  20774. end;
  20775. procedure TSQLPropInfoRTTIDynArray.GetBinary(Instance: TObject; W: TFileBufferWriter);
  20776. var Value: RawByteString;
  20777. begin
  20778. Serialize(Instance,Value,true);
  20779. if fObjArray<>nil then
  20780. W.Write(Value) else
  20781. W.WriteBinary(Value);
  20782. end;
  20783. function TSQLPropInfoRTTIDynArray.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  20784. var tmp: RawByteString;
  20785. begin
  20786. Serialize(Instance,tmp,true);
  20787. result := crc32c(0,pointer(tmp),length(tmp));
  20788. end;
  20789. procedure TSQLPropInfoRTTIDynArray.GetValueVar(Instance: TObject;
  20790. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  20791. begin
  20792. Serialize(Instance,RawByteString(result),false);
  20793. if fObjArray=nil then
  20794. BinaryToText(result,ToSQL,wasSQLString);
  20795. end;
  20796. {$ifndef NOVARIANTS}
  20797. procedure TSQLPropInfoRTTIDynArray.GetVariant(Instance: TObject; var Dest: Variant);
  20798. var json: RawUTF8;
  20799. begin
  20800. VarClear(Dest);
  20801. json := GetDynArray(Instance).SaveToJSON;
  20802. TDocVariantData(Dest).InitJSONInPlace(pointer(json),JSON_OPTIONS_FAST);
  20803. end;
  20804. procedure TSQLPropInfoRTTIDynArray.SetVariant(Instance: TObject; const Source: Variant);
  20805. var json: RawUTF8;
  20806. begin
  20807. VariantSaveJSON(Source,twJSONEscape,json);
  20808. GetDynArray(Instance).LoadFromJSON(pointer(json));
  20809. end;
  20810. {$endif NOVARIANTS}
  20811. procedure TSQLPropInfoRTTIDynArray.NormalizeValue(var Value: RawUTF8);
  20812. begin // do nothing: should already be normalized
  20813. end;
  20814. function TSQLPropInfoRTTIDynArray.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt;
  20815. var da1,da2: TDynArray;
  20816. begin
  20817. if Item1=Item2 then
  20818. result := 0 else
  20819. if Item1=nil then
  20820. result := -1 else
  20821. if Item2=nil then
  20822. result := 1 else begin
  20823. GetDynArray(Item1,da1);
  20824. GetDynArray(Item2,da2);
  20825. if da1.Equals(da2) then
  20826. result := 0 else
  20827. result := PtrInt(Item1)-PtrInt(Item2); // pseudo comparison
  20828. end;
  20829. end;
  20830. function TSQLPropInfoRTTIDynArray.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  20831. var tmp: TSynTempBuffer; // LoadFromJSON() may change the input buffer
  20832. da: TDynArray;
  20833. begin
  20834. GetDynArray(Instance,da);
  20835. if fObjArray<>nil then begin
  20836. FromVarString(PByte(P),tmp);
  20837. da.LoadFromJSON(tmp.buf);
  20838. tmp.Done;
  20839. result := P;
  20840. end else
  20841. result := da.LoadFrom(P);
  20842. end;
  20843. procedure TSQLPropInfoRTTIDynArray.SetValue(Instance: TObject;
  20844. Value: PUTF8Char; wasString: boolean);
  20845. var tmp: TSynTempBuffer;
  20846. da: TDynArray;
  20847. begin
  20848. GetDynArray(Instance,da);
  20849. if Value=nil then
  20850. da.Clear else
  20851. try
  20852. if (fObjArray=nil) and Base64MagicCheckAndDecode(Value,tmp) then
  20853. da.LoadFrom(tmp.buf) else begin
  20854. tmp.Init(Value);
  20855. da.LoadFromJSON(tmp.buf);
  20856. end;
  20857. finally
  20858. tmp.Done;
  20859. end;
  20860. end;
  20861. function TSQLPropInfoRTTIDynArray.SetFieldSQLVar(Instance: TObject;
  20862. const aValue: TSQLVar): boolean;
  20863. begin
  20864. if aValue.VType=ftBlob then
  20865. result := GetDynArray(Instance).LoadFrom(aValue.VBlob)<>nil else
  20866. result := inherited SetFieldSQLVar(Instance,aValue);
  20867. end;
  20868. procedure TSQLPropInfoRTTIDynArray.GetJSONValues(Instance: TObject;
  20869. W: TJSONSerializer);
  20870. var tmp: RawByteString;
  20871. begin
  20872. if jwoAsJsonNotAsString in W.fSQLRecordOptions then
  20873. W.AddDynArrayJSON(fPropType,GetFieldAddr(Instance)^) else
  20874. if fObjArray<>nil then
  20875. W.AddDynArrayJSONAsString(fPropType,GetFieldAddr(Instance)^) else begin
  20876. Serialize(Instance,tmp,false);
  20877. W.WrBase64(pointer(tmp),Length(tmp),true); // withMagic=true -> add ""
  20878. end;
  20879. end;
  20880. procedure TSQLPropInfoRTTIDynArray.GetFieldSQLVar(Instance: TObject;
  20881. var aValue: TSQLVar; var temp: RawByteString);
  20882. begin
  20883. Serialize(Instance,temp,false);
  20884. if fObjArray<>nil then begin
  20885. aValue.VType := ftUTF8; // JSON
  20886. aValue.VText := pointer(temp);
  20887. end else begin
  20888. aValue.VType := ftBlob; // binary
  20889. aValue.VBlob := pointer(temp);
  20890. aValue.VBlobLen := length(temp);
  20891. end;
  20892. end;
  20893. function TSQLPropInfoRTTIDynArray.GetDynArrayElemType: PTypeInfo;
  20894. begin
  20895. result := GetDynArray(nil).ElemType;
  20896. end;
  20897. {$ifndef NOVARIANTS}
  20898. function NullableInteger(const Value: Int64): TNullableInteger;
  20899. begin
  20900. PVariant(@result)^ := Value;
  20901. end;
  20902. function NullableIntegerIsEmptyOrNull(const V: TNullableInteger): Boolean;
  20903. begin
  20904. result := VarDataIsEmptyOrNull(@V);
  20905. end;
  20906. function NullableIntegerToValue(const V: TNullableInteger; out Value: Int64): Boolean;
  20907. begin
  20908. Value := 0;
  20909. result := (not VarDataIsEmptyOrNull(@V)) and VariantToInt64(PVariant(@V)^,Value);
  20910. end;
  20911. function NullableIntegerToValue(const V: TNullableInteger): Int64;
  20912. begin
  20913. VariantToInt64(PVariant(@V)^,result);
  20914. end;
  20915. function NullableBoolean(Value: boolean): TNullableBoolean;
  20916. begin
  20917. PVariant(@result)^ := Value;
  20918. end;
  20919. function NullableBooleanIsEmptyOrNull(const V: TNullableBoolean): Boolean;
  20920. begin
  20921. result := VarDataIsEmptyOrNull(@V);
  20922. end;
  20923. function NullableBooleanToValue(const V: TNullableBoolean; out Value: Boolean): Boolean;
  20924. begin
  20925. Value := false;
  20926. result := (not VarDataIsEmptyOrNull(@V)) and VariantToBoolean(PVariant(@V)^,Value);
  20927. end;
  20928. function NullableBooleanToValue(const V: TNullableBoolean): Boolean;
  20929. begin
  20930. VariantToBoolean(PVariant(@V)^,result);
  20931. end;
  20932. function NullableFloat(const Value: double): TNullableFloat;
  20933. begin
  20934. PVariant(@result)^ := Value;
  20935. end;
  20936. function NullableFloatIsEmptyOrNull(const V: TNullableFloat): Boolean;
  20937. begin
  20938. result := VarDataIsEmptyOrNull(@V);
  20939. end;
  20940. function NullableFloatToValue(const V: TNullableFloat; out Value: Double): Boolean;
  20941. begin
  20942. Value := 0;
  20943. result := (not VarDataIsEmptyOrNull(@V)) and VariantToDouble(PVariant(@V)^,Value);
  20944. end;
  20945. function NullableFloatToValue(const V: TNullableFloat): Double;
  20946. begin
  20947. VariantToDouble(PVariant(@V)^,result);
  20948. end;
  20949. function NullableCurrency(const Value: currency): TNullableCurrency;
  20950. begin
  20951. PVariant(@result)^ := Value;
  20952. end;
  20953. function NullableCurrencyIsEmptyOrNull(const V: TNullableCurrency): Boolean;
  20954. begin
  20955. result := VarDataIsEmptyOrNull(@V);
  20956. end;
  20957. function NullableCurrencyToValue(const V: TNullableCurrency; out Value: currency): Boolean;
  20958. begin
  20959. Value := 0;
  20960. result := (not VarDataIsEmptyOrNull(@V)) and VariantToCurrency(PVariant(@V)^,Value);
  20961. end;
  20962. function NullableCurrencyToValue(const V: TNullableCurrency): currency;
  20963. begin
  20964. VariantToCurrency(PVariant(@V)^,result);
  20965. end;
  20966. function NullableDateTime(const Value: TDateTime): TNullableDateTime;
  20967. begin
  20968. PVariant(@result)^ := Value;
  20969. end;
  20970. function NullableDateTimeIsEmptyOrNull(const V: TNullableDateTime): Boolean;
  20971. begin
  20972. result := VarDataIsEmptyOrNull(@V);
  20973. end;
  20974. function NullableDateTimeToValue(const V: TNullableDateTime; out Value: TDateTime): Boolean;
  20975. begin
  20976. Value := 0;
  20977. result := (not VarDataIsEmptyOrNull(@V)) and VariantToDouble(PVariant(@V)^,Double(Value));
  20978. end;
  20979. function NullableDateTimeToValue(const V: TNullableDateTime): TDateTime;
  20980. begin
  20981. VariantToDouble(PVariant(@V)^,Double(result));
  20982. end;
  20983. function NullableTimeLog(const Value: TTimeLog): TNullableTimeLog;
  20984. begin
  20985. PVariant(@result)^ := Value;
  20986. end;
  20987. function NullableTimeLogIsEmptyOrNull(const V: TNullableTimeLog): Boolean;
  20988. begin
  20989. result := VarDataIsEmptyOrNull(@V);
  20990. end;
  20991. function NullableTimeLogToValue(const V: TNullableTimeLog; out Value: TTimeLog): Boolean;
  20992. begin
  20993. Value := 0;
  20994. result := (not VarDataIsEmptyOrNull(@V)) and VariantToInt64(PVariant(@V)^,Int64(Value));
  20995. end;
  20996. function NullableTimeLogToValue(const V: TNullableTimeLog): TTimeLog;
  20997. begin
  20998. VariantToInt64(PVariant(@V)^,Int64(result));
  20999. end;
  21000. function NullableUTF8Text(const Value: RawUTF8): TNullableUTF8Text;
  21001. begin
  21002. VarClear(PVariant(@result)^);
  21003. TVarData(result).VType := varString;
  21004. TVarData(result).VAny := nil; // avoid GPF below
  21005. RawUTF8(TVarData(result).VAny) := Value;
  21006. end;
  21007. function NullableUTF8TextIsEmptyOrNull(const V: TNullableUTF8Text): Boolean;
  21008. begin
  21009. result := VarDataIsEmptyOrNull(@V);
  21010. end;
  21011. function NullableUTF8TextToValue(const V: TNullableUTF8Text; out Value: RawUTF8): boolean;
  21012. begin
  21013. result := (not VarDataIsEmptyOrNull(@V)) and VariantToUTF8(PVariant(@V)^,Value);
  21014. end;
  21015. function NullableUTF8TextToValue(const V: TNullableUTF8Text): RawUTF8;
  21016. var dummy: boolean;
  21017. begin
  21018. if VarDataIsEmptyOrNull(@V) then // VariantToUTF8() would return 'null'
  21019. result := '' else
  21020. VariantToUTF8(PVariant(@V)^,result,dummy);
  21021. end;
  21022. { TSQLPropInfoRTTIVariant }
  21023. constructor TSQLPropInfoRTTIVariant.Create(aPropInfo: PPropInfo; aPropIndex: integer;
  21024. aSQLFieldType: TSQLFieldType);
  21025. begin
  21026. inherited;
  21027. if aSQLFieldType=sftVariant then
  21028. fDocVariantOptions := JSON_OPTIONS_FAST else
  21029. fSQLFieldType := sftNullable; // TNullable* will use fSQLFieldTypeStored
  21030. end;
  21031. procedure TSQLPropInfoRTTIVariant.CopySameClassProp(Source: TObject;
  21032. DestInfo: TSQLPropInfo; Dest: TObject);
  21033. var value: Variant;
  21034. begin
  21035. fPropInfo.GetVariantProp(Source,value);
  21036. TSQLPropInfoRTTIVariant(DestInfo).fPropInfo.SetVariantProp(Dest,value);
  21037. end;
  21038. procedure TSQLPropInfoRTTIVariant.GetBinary(Instance: TObject;
  21039. W: TFileBufferWriter);
  21040. var value: Variant;
  21041. begin
  21042. fPropInfo.GetVariantProp(Instance,value);
  21043. W.Write(value);
  21044. end;
  21045. function TSQLPropInfoRTTIVariant.GetHash(Instance: TObject;
  21046. CaseInsensitive: boolean): cardinal;
  21047. var Up: array[byte] of AnsiChar; // avoid slow heap allocation
  21048. value: Variant;
  21049. procedure ComplexType;
  21050. var tmp: RawUTF8;
  21051. begin // slow but always working conversion to string
  21052. tmp := VariantSaveJSON(value,twNone);
  21053. if CaseInsensitive then
  21054. result := crc32c(0,Up,UpperCopy255(Up,tmp)-Up) else
  21055. result := crc32c(0,pointer(tmp),length(tmp));
  21056. end;
  21057. begin
  21058. fPropInfo.GetVariantProp(Instance,value);
  21059. with TVarData(value) do
  21060. case VType of
  21061. varNull, varEmpty:
  21062. result := 0;
  21063. varSmallint, varWord, varBoolean:
  21064. result := VWord;
  21065. varShortInt, varByte:
  21066. result := VByte;
  21067. varLongWord, varInteger, varSingle:
  21068. result := varLongWord;
  21069. varInt64, varDouble, varDate, varCurrency:
  21070. result := crc32c(0,@VInt64,sizeof(Int64));
  21071. varString:
  21072. if CaseInsensitive then
  21073. result := crc32c(0,Up,UpperCopy255Buf(Up,VString,length(RawUTF8(VString)))-Up) else
  21074. result := crc32c(0,VString,length(RawUTF8(VString)));
  21075. varOleStr {$ifdef HASVARUSTRING}, varUString{$endif}:
  21076. if CaseInsensitive then
  21077. result := crc32c(0,Up,UpperCopy255W(Up,VOleStr,StrLenW(VOleStr))-Up) else
  21078. result := crc32c(0,VAny,StrLenW(VOleStr)*2);
  21079. else
  21080. ComplexType;
  21081. end;
  21082. end;
  21083. procedure TSQLPropInfoRTTIVariant.GetJSONValues(Instance: TObject;
  21084. W: TJSONSerializer);
  21085. var value: Variant;
  21086. backup: TTextWriterOptions;
  21087. begin
  21088. fPropInfo.GetVariantProp(Instance,value);
  21089. backup := W.CustomOptions;
  21090. if jwoAsJsonNotAsString in W.fSQLRecordOptions then
  21091. W.CustomOptions := backup+[twoForceJSONStandard]-[twoForceJSONExtended];
  21092. W.AddVariant(value,twJSONEscape); // even sftNullable should escape strings
  21093. W.CustomOptions := backup;
  21094. end;
  21095. procedure TSQLPropInfoRTTIVariant.GetValueVar(Instance: TObject;
  21096. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  21097. var wasString: boolean;
  21098. value: Variant;
  21099. begin
  21100. fPropInfo.GetVariantProp(Instance,value);
  21101. VariantToUTF8(value,result,wasString);
  21102. if wasSQLString<>nil then
  21103. if fSQLFieldType=sftNullable then
  21104. // only TNullableUTF8Text and TNullableDateTime would be actual text
  21105. wasSQLString^ := (fSQLDBFieldType in TEXT_DBFIELDS) and
  21106. not VarIsEmptyOrNull(value) else
  21107. // from SQL point of view, variant columns are TEXT or NULL
  21108. wasSQLString^ := not VarIsEmptyOrNull(value);
  21109. end;
  21110. procedure TSQLPropInfoRTTIVariant.GetVariant(Instance: TObject;
  21111. var Dest: Variant);
  21112. begin
  21113. fPropInfo.GetVariantProp(Instance,Dest);
  21114. end;
  21115. procedure TSQLPropInfoRTTIVariant.NormalizeValue(var Value: RawUTF8);
  21116. begin // content should be already normalized
  21117. end;
  21118. function TSQLPropInfoRTTIVariant.CompareValue(Item1, Item2: TObject;
  21119. CaseInsensitive: boolean): PtrInt;
  21120. procedure Generic;
  21121. var V1,V2: variant;
  21122. begin
  21123. fPropInfo.GetVariantProp(Item1,V1);
  21124. fPropInfo.GetVariantProp(Item2,V2);
  21125. result := SortDynArrayVariantComp(TVarData(V1),TVarData(V2),CaseInsensitive);
  21126. end;
  21127. begin
  21128. if Item1=Item2 then
  21129. result := 0 else
  21130. if Item1=nil then
  21131. result := -1 else
  21132. if Item2=nil then
  21133. result := 1 else
  21134. if fGetterIsFieldPropOffset<>0 then // avoid any temporary variable
  21135. result := SortDynArrayVariantComp(PVarData(PtrUInt(Item1)+fGetterIsFieldPropOffset)^,
  21136. PVarData(PtrUInt(Item2)+fGetterIsFieldPropOffset)^,CaseInsensitive) else
  21137. Generic;
  21138. end;
  21139. function TSQLPropInfoRTTIVariant.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  21140. var value: Variant;
  21141. begin
  21142. if fSQLFieldType=sftNullable then
  21143. result := VariantLoad(value,P,nil) else
  21144. result := VariantLoad(value,P,@DocVariantOptions);
  21145. fPropInfo.SetVariantProp(Instance,value);
  21146. end;
  21147. procedure TSQLPropInfoRTTIVariant.SetValue(Instance: TObject; Value: PUTF8Char;
  21148. wasString: boolean);
  21149. begin
  21150. SetValuePtr(Instance,Value,StrLen(Value),wasString);
  21151. end;
  21152. procedure TSQLPropInfoRTTIVariant.SetValueVar(Instance: TObject;
  21153. const Value: RawUTF8; wasString: boolean);
  21154. begin
  21155. SetValuePtr(Instance,pointer(Value),length(Value),wasString);
  21156. end;
  21157. procedure TSQLPropInfoRTTIVariant.SetValuePtr(Instance: TObject; Value: PUTF8Char;
  21158. ValueLen: integer; wasString: boolean);
  21159. var tmp: TSynTempBuffer;
  21160. V: Variant;
  21161. begin
  21162. if ValueLen>0 then begin
  21163. tmp.Init(Value,ValueLen);
  21164. try
  21165. if fSQLFieldType=sftNullable then
  21166. GetVariantFromJSON(tmp.buf,wasString,V,nil) else
  21167. GetVariantFromJSON(tmp.buf,wasString,V,@DocVariantOptions);
  21168. fPropInfo.SetVariantProp(Instance,V);
  21169. finally
  21170. tmp.Done;
  21171. end;
  21172. end else begin
  21173. TVarData(V).VType := varNull; // TEXT or NULL: see GetValueVar()
  21174. fPropInfo.SetVariantProp(Instance,V);
  21175. end;
  21176. end;
  21177. procedure TSQLPropInfoRTTIVariant.SetVariant(Instance: TObject;
  21178. const Source: Variant);
  21179. begin
  21180. fPropInfo.SetVariantProp(Instance,Source);
  21181. end;
  21182. {$endif NOVARIANTS}
  21183. { TSQLPropInfoCustom }
  21184. function TSQLPropInfoCustom.GetFieldAddr(Instance: TObject): pointer;
  21185. begin
  21186. if Instance=nil then
  21187. result := nil else
  21188. result := PAnsiChar(Instance)+fOffset;
  21189. end;
  21190. constructor TSQLPropInfoCustom.Create(const aName: RawUTF8;
  21191. aSQLFieldType: TSQLFieldType; aAttributes: TSQLPropInfoAttributes;
  21192. aFieldWidth, aPropIndex: integer; aProperty: pointer;
  21193. aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data);
  21194. begin
  21195. inherited Create(aName,aSQLFieldType,aAttributes,aFieldWidth,aPropIndex);
  21196. fOffset := PtrUInt(aProperty);
  21197. if (Assigned(aData2Text) and not Assigned(aText2Data)) or
  21198. (Assigned(aText2Data) and not Assigned(aData2Text)) then
  21199. raise EORMException.CreateUTF8(
  21200. 'Invalid %.Create: expecting both Data2Text/Text2Data',[self]);
  21201. fData2Text := aData2Text;
  21202. fText2Data := aText2Data;
  21203. end;
  21204. procedure TSQLPropInfoCustom.TextToBinary(Value: PUTF8Char; var result: RawByteString);
  21205. begin
  21206. if Assigned(fText2Data) then
  21207. fText2Data(Value,result) else
  21208. result := BlobToTSQLRawBlob(Value);
  21209. end;
  21210. procedure TSQLPropInfoCustom.BinaryToText(var Value: RawUTF8; ToSQL: boolean;
  21211. wasSQLString: PBoolean);
  21212. begin
  21213. if Assigned(fData2Text) then
  21214. fData2Text(UniqueRawUTF8(Value),length(Value),Value) else
  21215. inherited BinaryToText(Value,ToSQL,wasSQLString);
  21216. end;
  21217. { TSQLPropInfoRecordRTTI }
  21218. procedure TSQLPropInfoRecordRTTI.CopySameClassProp(Source: TObject;
  21219. DestInfo: TSQLPropInfo; Dest: TObject);
  21220. begin
  21221. if TSQLPropInfoRecordRTTI(DestInfo).fTypeInfo=fTypeInfo then
  21222. RecordCopy(TSQLPropInfoRecordRTTI(DestInfo).GetFieldAddr(Dest)^,
  21223. GetFieldAddr(Source)^,fTypeInfo) else
  21224. inherited CopySameClassProp(Source,DestInfo,Dest);
  21225. end;
  21226. function TSQLPropInfoRecordRTTI.GetSQLFieldRTTITypeName: RawUTF8;
  21227. begin
  21228. if fTypeInfo=nil then
  21229. result := inherited GetSQLFieldRTTITypeName else
  21230. result := ToUTF8(fTypeInfo^.Name);
  21231. end;
  21232. constructor TSQLPropInfoRecordRTTI.Create(aRecordInfo: PTypeInfo;
  21233. const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer;
  21234. aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer;
  21235. aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data);
  21236. begin
  21237. if (aRecordInfo=nil) or not(aRecordInfo^.Kind in tkRecordTypes) then
  21238. raise EORMException.CreateUTF8('%.Create: Invalid type information for %',[self,aName]);
  21239. inherited Create(aName,sftBlobCustom,aAttributes,aFieldWidth,aPropertyIndex,
  21240. aPropertyPointer,aData2Text,aText2Data);
  21241. fTypeInfo := aRecordInfo;
  21242. end;
  21243. procedure TSQLPropInfoRecordRTTI.GetBinary(Instance: TObject; W: TFileBufferWriter);
  21244. var Value: RawByteString;
  21245. begin
  21246. Value := RecordSave(GetFieldAddr(Instance)^,fTypeInfo);
  21247. W.Write(pointer(Value),length(Value));
  21248. end;
  21249. function TSQLPropInfoRecordRTTI.GetHash(Instance: TObject;
  21250. CaseInsensitive: boolean): cardinal;
  21251. var Value: RawByteString;
  21252. begin
  21253. Value := RecordSave(GetFieldAddr(Instance)^,fTypeInfo);
  21254. result := crc32c(0,pointer(Value),length(Value));
  21255. end;
  21256. {$ifndef NOVARIANTS}
  21257. procedure TSQLPropInfoRecordRTTI.GetVariant(Instance: TObject; var Dest: Variant);
  21258. begin
  21259. Dest := RecordSave(GetFieldAddr(Instance)^,fTypeInfo);
  21260. end;
  21261. procedure TSQLPropInfoRecordRTTI.SetVariant(Instance: TObject; const Source: Variant);
  21262. begin
  21263. if TVarData(Source).VType=varString then
  21264. RecordLoad(GetFieldAddr(Instance)^,TVarData(Source).VAny,fTypeInfo) else
  21265. RecordClear(GetFieldAddr(Instance)^,fTypeInfo);
  21266. end;
  21267. {$endif NOVARIANTS}
  21268. procedure TSQLPropInfoRecordRTTI.NormalizeValue(var Value: RawUTF8);
  21269. begin // a BLOB should already be normalized
  21270. end;
  21271. function TSQLPropInfoRecordRTTI.CompareValue(Item1, Item2: TObject;
  21272. CaseInsensitive: boolean): PtrInt;
  21273. begin
  21274. if RecordEquals(GetFieldAddr(Item1)^,GetFieldAddr(Item2)^,fTypeInfo) then
  21275. result := 0 else
  21276. result := PtrInt(Item1)-PtrInt(Item2); // pseudo comparison
  21277. end;
  21278. function TSQLPropInfoRecordRTTI.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  21279. begin
  21280. result := RecordLoad(GetFieldAddr(Instance)^,P,fTypeInfo);
  21281. end;
  21282. procedure TSQLPropInfoRecordRTTI.SetValue(Instance: TObject; Value: PUTF8Char;
  21283. wasString: boolean);
  21284. var data: RawByteString;
  21285. begin
  21286. TextToBinary(Value,data);
  21287. RecordLoad(GetFieldAddr(Instance)^,pointer(data),fTypeInfo);
  21288. end;
  21289. procedure TSQLPropInfoRecordRTTI.GetValueVar(Instance: TObject;
  21290. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  21291. begin
  21292. result := RecordSave(GetFieldAddr(Instance)^,fTypeInfo);
  21293. BinaryToText(result,ToSQL,wasSQLString);
  21294. end;
  21295. function TSQLPropInfoRecordRTTI.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  21296. begin
  21297. if aValue.VType=ftBlob then
  21298. result := RecordLoad(GetFieldAddr(Instance)^,aValue.VBlob,fTypeInfo)<>nil else
  21299. result := inherited SetFieldSQLVar(Instance,aValue);
  21300. end;
  21301. procedure TSQLPropInfoRecordRTTI.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  21302. var temp: RawByteString);
  21303. begin
  21304. temp := RecordSave(GetFieldAddr(Instance)^,fTypeInfo);
  21305. aValue.VType := ftBlob;
  21306. aValue.VBlob := pointer(temp);
  21307. aValue.VBlobLen := length(temp);
  21308. end;
  21309. { TSQLPropInfoRecordFixedSize }
  21310. procedure TSQLPropInfoRecordFixedSize.CopySameClassProp(Source: TObject;
  21311. DestInfo: TSQLPropInfo; Dest: TObject);
  21312. begin
  21313. if TSQLPropInfoRecordFixedSize(DestInfo).fTypeInfo=fTypeInfo then
  21314. MoveFast(GetFieldAddr(Source)^,
  21315. TSQLPropInfoRecordFixedSize(DestInfo).GetFieldAddr(Dest)^,fRecordSize) else
  21316. inherited CopySameClassProp(Source,DestInfo,Dest);
  21317. end;
  21318. function TSQLPropInfoRecordFixedSize.GetSQLFieldRTTITypeName: RawUTF8;
  21319. begin
  21320. if fTypeInfo=nil then
  21321. result := inherited GetSQLFieldRTTITypeName else
  21322. result := ToUTF8(fTypeInfo^.Name);
  21323. end;
  21324. constructor TSQLPropInfoRecordFixedSize.Create(aRecordSize: cardinal;
  21325. const aName: RawUTF8; aPropertyIndex: integer;
  21326. aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes;
  21327. aFieldWidth: integer; aData2Text: TOnSQLPropInfoRecord2Text;
  21328. aText2Data: TOnSQLPropInfoRecord2Data);
  21329. begin
  21330. if integer(aRecordSize)<=0 then
  21331. raise EORMException.CreateUTF8('%.Create: invalid % record size',[self,aRecordSize]);
  21332. fRecordSize := aRecordSize;
  21333. inherited Create(aName,sftBlobCustom,aAttributes,aFieldWidth,aPropertyIndex,
  21334. aPropertyPointer,aData2Text,aText2Data);
  21335. end;
  21336. procedure TSQLPropInfoRecordFixedSize.GetBinary(Instance: TObject; W: TFileBufferWriter);
  21337. begin
  21338. W.Write(GetFieldAddr(Instance),fRecordSize);
  21339. end;
  21340. function TSQLPropInfoRecordFixedSize.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal;
  21341. begin
  21342. result := crc32c(0,GetFieldAddr(Instance),fRecordSize);
  21343. end;
  21344. procedure TSQLPropInfoRecordFixedSize.GetValueVar(Instance: TObject;
  21345. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  21346. begin
  21347. SetRawUTF8(result,GetFieldAddr(Instance),fRecordSize);
  21348. BinaryToText(result,ToSQL,wasSQLString);
  21349. end;
  21350. {$ifndef NOVARIANTS}
  21351. procedure TSQLPropInfoRecordFixedSize.GetVariant(Instance: TObject;
  21352. var Dest: Variant);
  21353. var tmp: RawByteString;
  21354. begin
  21355. SetString(tmp,PAnsiChar(GetFieldAddr(Instance)),fRecordSize);
  21356. Dest := tmp;
  21357. end;
  21358. procedure TSQLPropInfoRecordFixedSize.SetVariant(Instance: TObject;
  21359. const Source: Variant);
  21360. begin
  21361. if TVarData(Source).VType=varString then
  21362. MoveFast(TVarData(Source).VAny^,GetFieldAddr(Instance)^,fRecordSize) else
  21363. FillcharFast(GetFieldAddr(Instance)^,fRecordSize,0);
  21364. end;
  21365. {$endif NOVARIANTS}
  21366. procedure TSQLPropInfoRecordFixedSize.NormalizeValue(var Value: RawUTF8);
  21367. begin // a BLOB should already be normalized
  21368. end;
  21369. function TSQLPropInfoRecordFixedSize.CompareValue(Item1, Item2: TObject;
  21370. CaseInsensitive: boolean): PtrInt;
  21371. var i: Integer;
  21372. P1,P2: PByteArray;
  21373. begin
  21374. if (Item1=Item2) or (fRecordSize=0) then
  21375. result := 0 else
  21376. if Item1=nil then
  21377. result := -1 else
  21378. if Item2=nil then
  21379. result := 1 else begin
  21380. result := 0;
  21381. P1 := GetFieldAddr(Item1);
  21382. P2 := GetFieldAddr(Item2);
  21383. for i := 0 to fRecordSize-1 do begin
  21384. result := P1^[i]-P2^[i];
  21385. if result<>0 then
  21386. exit;
  21387. end;
  21388. end;
  21389. end;
  21390. function TSQLPropInfoRecordFixedSize.SetBinary(Instance: TObject; P: PAnsiChar): PAnsiChar;
  21391. begin
  21392. if P=nil then
  21393. FillcharFast(GetFieldAddr(Instance)^,fRecordSize,0) else
  21394. MoveFast(P^,GetFieldAddr(Instance)^,fRecordSize);
  21395. result := P+fRecordSize;
  21396. end;
  21397. procedure TSQLPropInfoRecordFixedSize.SetValue(Instance: TObject; Value: PUTF8Char;
  21398. wasString: boolean);
  21399. var data: RawByteString;
  21400. begin
  21401. TextToBinary(Value,data);
  21402. Value := pointer(data);
  21403. if Value=nil then
  21404. FillcharFast(GetFieldAddr(Instance)^,fRecordSize,0) else
  21405. MoveFast(Value^,GetFieldAddr(Instance)^,fRecordSize);
  21406. end;
  21407. function TSQLPropInfoRecordFixedSize.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean;
  21408. begin
  21409. if aValue.VType=ftBlob then begin
  21410. result := aValue.VBlobLen=fRecordSize;
  21411. if result then
  21412. MoveFast(aValue.VBlob^,GetFieldAddr(Instance)^,fRecordSize)
  21413. end else
  21414. result := inherited SetFieldSQLVar(Instance,aValue);
  21415. end;
  21416. procedure TSQLPropInfoRecordFixedSize.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar;
  21417. var temp: RawByteString);
  21418. begin
  21419. SetString(temp,PAnsiChar(GetFieldAddr(Instance)),fRecordSize);
  21420. aValue.VType := ftBlob;
  21421. aValue.VBlob := pointer(temp);
  21422. aValue.VBlobLen := length(temp);
  21423. end;
  21424. { TSQLPropInfoCustomJSON }
  21425. constructor TSQLPropInfoCustomJSON.Create(aPropInfo: PPropInfo; aPropIndex: integer);
  21426. var attrib: TSQLPropInfoAttributes;
  21427. begin
  21428. byte(attrib) := 0;
  21429. if aPropInfo^.IsStored(nil)=AS_UNIQUE then
  21430. Include(attrib,aIsUnique); // property MyProperty: RawUTF8 stored AS_UNIQUE;ieldWidth=10
  21431. Create(aPropInfo^.TypeInfo,ToUTF8(aPropInfo^.Name),
  21432. aPropIndex,aPropInfo^.GetFieldAddr(nil),attrib,aPropInfo^.Index);
  21433. end;
  21434. constructor TSQLPropInfoCustomJSON.Create(aTypeInfo: PTypeInfo;
  21435. const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer;
  21436. aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer);
  21437. begin
  21438. inherited Create(aName,sftUTF8Custom,aAttributes,aFieldWidth,aPropertyIndex,
  21439. aPropertyPointer,nil,nil);
  21440. fTypeInfo := aTypeInfo;
  21441. SetCustomParser(TJSONCustomParserRTTI.CreateFromRTTI(aName,aTypeInfo,0));
  21442. end;
  21443. constructor TSQLPropInfoCustomJSON.Create(const aTypeName, aName: RawUTF8;
  21444. aPropertyIndex: integer; aPropertyPointer: pointer;
  21445. aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer);
  21446. begin
  21447. inherited Create(aName,sftUTF8Custom,aAttributes,aFieldWidth,aPropertyIndex,
  21448. aPropertyPointer,nil,nil);
  21449. SetCustomParser(TJSONCustomParserRTTI.CreateFromTypeName(aName,aTypeName));
  21450. end;
  21451. function TSQLPropInfoCustomJSON.GetSQLFieldRTTITypeName: RawUTF8;
  21452. begin
  21453. if fTypeInfo=nil then
  21454. result := inherited GetSQLFieldRTTITypeName else
  21455. result := ToUTF8(fTypeInfo^.Name);
  21456. end;
  21457. procedure TSQLPropInfoCustomJSON.SetCustomParser(
  21458. aCustomParser: TJSONCustomParserRTTI);
  21459. begin
  21460. if aCustomParser=nil then
  21461. raise EORMException.CreateUTF8('%.SetCustomParser: Invalid type information for %',
  21462. [self,Name]);
  21463. fCustomParser := aCustomParser;
  21464. end;
  21465. destructor TSQLPropInfoCustomJSON.Destroy;
  21466. begin
  21467. inherited;
  21468. fCustomParser.Free;
  21469. end;
  21470. procedure TSQLPropInfoCustomJSON.GetBinary(Instance: TObject;
  21471. W: TFileBufferWriter);
  21472. var JSON: RawUTF8;
  21473. begin
  21474. GetValueVar(Instance,false,JSON,nil);
  21475. W.Write(JSON);
  21476. end;
  21477. function TSQLPropInfoCustomJSON.SetBinary(Instance: TObject;
  21478. P: PAnsiChar): PAnsiChar;
  21479. begin
  21480. SetValue(Instance,pointer(FromVarString(PByte(P))),false);
  21481. result := P;
  21482. end;
  21483. procedure TSQLPropInfoCustomJSON.NormalizeValue(var Value: RawUTF8);
  21484. begin // do nothing: should already be normalized
  21485. end;
  21486. procedure TSQLPropInfoCustomJSON.GetJSONValues(Instance: TObject;
  21487. W: TJSONSerializer);
  21488. var Data: PByte;
  21489. begin
  21490. Data := GetFieldAddr(Instance);
  21491. fCustomParser.WriteOneLevel(W,Data,
  21492. [soReadIgnoreUnknownFields,soCustomVariantCopiedByReference]);
  21493. end;
  21494. procedure TSQLPropInfoCustomJSON.GetValueVar(Instance: TObject;
  21495. ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean);
  21496. var W: TJSONSerializer;
  21497. begin
  21498. W := TJSONSerializer.CreateOwnedStream;
  21499. try
  21500. GetJSONValues(Instance,W);
  21501. W.SetText(result);
  21502. if wasSQLString<>nil then
  21503. wasSQLString^ := (result<>'') and (result[1]='"');
  21504. finally
  21505. W.Free;
  21506. end;
  21507. end;
  21508. procedure TSQLPropInfoCustomJSON.SetValue(Instance: TObject;
  21509. Value: PUTF8Char; wasString: boolean);
  21510. var Data: PByte;
  21511. tmp: RawUTF8;
  21512. begin
  21513. Data := GetFieldAddr(Instance);
  21514. if Value<>nil then
  21515. if ((Value[0]<>'{')or(Value[StrLen(Value)-1]<>'}')) and
  21516. ((Value[0]<>'[')or(Value[StrLen(Value)-1]<>']')) then begin
  21517. QuotedStr(Value,'"',tmp);
  21518. Value := pointer(tmp);
  21519. end;
  21520. fCustomParser.ReadOneLevel(Value,Data,
  21521. [soReadIgnoreUnknownFields,soCustomVariantCopiedByReference]);
  21522. end;
  21523. { TSQLPropInfoList }
  21524. constructor TSQLPropInfoList.Create(aTable: TClass; aOptions: TSQLPropInfoListOptions);
  21525. begin
  21526. fTable := aTable;
  21527. fOptions := aOptions;
  21528. if pilSubClassesFlattening in fOptions then
  21529. InternalAddParentsFirst(aTable,nil) else
  21530. InternalAddParentsFirst(aTable);
  21531. end;
  21532. destructor TSQLPropInfoList.Destroy;
  21533. var i: integer;
  21534. begin
  21535. for i := 0 to fCount-1 do
  21536. fList[i].Free;
  21537. inherited;
  21538. end;
  21539. procedure TSQLPropInfoList.InternalAddParentsFirst(aClassType: TClass;
  21540. aFlattenedProps: PPropInfoDynArray);
  21541. var P: PPropInfo;
  21542. i,prev: Integer;
  21543. begin
  21544. if aClassType=nil then
  21545. exit; // no RTTI information (e.g. reached TObject level)
  21546. if not (pilSingleHierarchyLevel in fOptions) then
  21547. InternalAddParentsFirst(aClassType.ClassParent,aFlattenedProps);
  21548. for i := 1 to InternalClassPropInfo(aClassType,P) do begin
  21549. if (P^.PropType^.Kind=tkClass) and
  21550. (P^.PropType^.ClassSQLFieldType in [sftObject,sftUnknown]) then begin
  21551. prev := PtrArrayAdd(aFlattenedProps,P);
  21552. InternalAddParentsFirst(P^.PropType^.ClassType^.ClassType,aFlattenedProps);
  21553. SetLength(aFlattenedProps,prev);
  21554. end else
  21555. if (pilIgnoreIfGetter in fOptions) and not P^.GetterIsField then
  21556. continue else
  21557. Add(TSQLPropInfoRTTI.CreateFrom(P,Count,fOptions,aFlattenedProps));
  21558. P := P^.Next;
  21559. end;
  21560. end;
  21561. procedure TSQLPropInfoList.InternalAddParentsFirst(aClassType: TClass);
  21562. var P: PPropInfo;
  21563. i: Integer;
  21564. begin
  21565. if aClassType=nil then
  21566. exit; // no RTTI information (e.g. reached TObject level)
  21567. if not (pilSingleHierarchyLevel in fOptions) then
  21568. InternalAddParentsFirst(aClassType.ClassParent);
  21569. for i := 1 to InternalClassPropInfo(aClassType,P) do begin
  21570. Add(TSQLPropInfoRTTI.CreateFrom(P,Count,fOptions,nil));
  21571. P := P^.Next;
  21572. end;
  21573. end;
  21574. function TSQLPropInfoList.Add(aItem: TSQLPropInfo): integer;
  21575. var f: integer;
  21576. begin
  21577. if aItem=nil then begin
  21578. result := -1;
  21579. exit;
  21580. end;
  21581. // check that this property is not an ID/RowID (handled separately)
  21582. if IsRowID(pointer(aItem.Name)) and not (pilAllowIDFields in fOptions) then
  21583. raise EModelException.CreateUTF8(
  21584. '%.Add: % should not include a "%" published property',[self,fTable,aItem.Name]);
  21585. // check that this property name is not already defined
  21586. for f := 0 to fCount-1 do
  21587. if IdemPropNameU(fList[f].Name,aItem.Name) then
  21588. raise EModelException.CreateUTF8('%.Add: % has duplicated name "%"',
  21589. [self,fTable,aItem.Name]);
  21590. // add to the internal list
  21591. result := fCount;
  21592. if result>=length(fList) then
  21593. SetLength(fList,result+result shr 3+32);
  21594. inc(fCount);
  21595. fList[result] := aItem;
  21596. fOrderedByName := nil; // force recompute sorted name array
  21597. end;
  21598. function TSQLPropInfoList.GetItem(aIndex: integer): TSQLPropInfo;
  21599. begin
  21600. if cardinal(aIndex)>=Cardinal(fCount) then
  21601. EORMException.Create('Invalid TSQLPropInfoList index');
  21602. result := fList[aIndex];
  21603. end;
  21604. procedure TSQLPropInfoList.QuickSortByName(L,R: PtrInt);
  21605. var I,J,P,Tmp: PtrInt;
  21606. pivot: PUTF8Char;
  21607. begin
  21608. if L<R then
  21609. repeat
  21610. I := L; J := R;
  21611. P := (L+R) shr 1;
  21612. repeat
  21613. pivot := pointer(fList[fOrderedByName[P]].fName);
  21614. while StrIComp(pointer(fList[fOrderedByName[I]].fName),pivot)<0 do inc(I);
  21615. while StrIComp(pointer(fList[fOrderedByName[J]].fName),pivot)>0 do dec(J);
  21616. if I <= J then begin
  21617. Tmp := fOrderedByName[J];
  21618. fOrderedByName[J] := fOrderedByName[I];
  21619. fOrderedByName[I] := Tmp;
  21620. if P=I then P := J else if P=J then P := I;
  21621. inc(I); dec(J);
  21622. end;
  21623. until I>J;
  21624. if L<J then
  21625. QuickSortByName(L,J);
  21626. L := I;
  21627. until I >= R;
  21628. end;
  21629. function TSQLPropInfoList.ByRawUTF8Name(const aName: RawUTF8): TSQLPropInfo;
  21630. var i: integer;
  21631. begin
  21632. i := IndexByName(pointer(aName));
  21633. if i<0 then
  21634. result := nil else
  21635. result := fList[i];
  21636. end;
  21637. function TSQLPropInfoList.ByName(aName: PUTF8Char): TSQLPropInfo;
  21638. var i: integer;
  21639. begin
  21640. i := IndexByName(aName);
  21641. if i<0 then
  21642. result := nil else
  21643. result := fList[i];
  21644. end;
  21645. function TSQLPropInfoList.IndexByName(aName: PUTF8Char): integer;
  21646. var cmp,L,R: integer;
  21647. begin
  21648. if (self<>nil) and (aName<>nil) and (fCount>0) then
  21649. if fCount<5 then begin
  21650. for result := 0 to fCount-1 do
  21651. if StrIComp(pointer(fList[result].fName),aName)=0 then
  21652. exit;
  21653. end else begin
  21654. if fOrderedByName=nil then begin
  21655. SetLength(fOrderedByName,fCount);
  21656. FillIncreasing(pointer(fOrderedByName),0,fCount);
  21657. QuickSortByName(0,fCount-1);
  21658. end;
  21659. L := 0;
  21660. R := fCount-1;
  21661. repeat
  21662. result := (L+R)shr 1;
  21663. cmp := StrIComp(pointer(fList[fOrderedByName[result]].fName),aName);
  21664. if cmp=0 then begin
  21665. result := fOrderedByName[result];
  21666. exit;
  21667. end;
  21668. if cmp<0 then
  21669. L := result+1 else
  21670. R := result-1;
  21671. until L>R;
  21672. end;
  21673. result := -1;
  21674. end;
  21675. function TSQLPropInfoList.IndexByName(const aName: RawUTF8): integer;
  21676. begin
  21677. result := IndexByName(pointer(aName));
  21678. end;
  21679. function TSQLPropInfoList.IndexByNameOrExcept(const aName: RawUTF8): integer;
  21680. begin
  21681. if IsRowID(pointer(aName)) then
  21682. result := -1 else begin
  21683. result := IndexByName(pointer(aName)); // fast binary search
  21684. if result<0 then
  21685. raise EORMException.CreateUTF8(
  21686. '%.IndexByNameOrExcept(%): unkwnown field in %',[self,aName,fTable]);
  21687. end;
  21688. end;
  21689. procedure TSQLPropInfoList.IndexesByNamesOrExcept(const aNames: array of RawUTF8;
  21690. const aIndexes: array of PInteger);
  21691. var i: integer;
  21692. begin
  21693. if high(aNames)<>high(aIndexes) then
  21694. raise EORMException.CreateUTF8('%.IndexesByNamesOrExcept(?)',[self]);
  21695. for i := 0 to high(aNames) do
  21696. if aIndexes[i]=nil then
  21697. raise EORMException.CreateUTF8('%.IndexesByNamesOrExcept(aIndexes[%]=nil)',[self,aNames[i]]) else
  21698. aIndexes[i]^ := IndexByNameOrExcept(aNames[i]);
  21699. end;
  21700. procedure TSQLPropInfoList.NamesToRawUTF8DynArray(var Names: TRawUTF8DynArray);
  21701. var i: integer;
  21702. begin
  21703. SetLength(Names,Count);
  21704. for i := 0 to Count-1 do
  21705. Names[i] := fList[i].Name;
  21706. end;
  21707. function TSQLPropInfoList.IndexByNameUnflattenedOrExcept(const aName: RawUTF8): integer;
  21708. begin
  21709. if pilSubClassesFlattening in fOptions then begin
  21710. for result := 0 to Count-1 do
  21711. if IdemPropNameU(List[result].NameUnflattened,aName) then
  21712. exit;
  21713. end else begin
  21714. result := IndexByName(pointer(aName)); // faster binary search
  21715. if result>=0 then
  21716. exit;
  21717. end;
  21718. raise EORMException.CreateUTF8(
  21719. '%.IndexByNameUnflattenedOrExcept(%): unkwnown field in %',[self,aName,fTable]);
  21720. end;
  21721. procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8);
  21722. begin // see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  21723. case Code of
  21724. HTML_CONTINUE: result := 'Continue';
  21725. HTML_SWITCHINGPROTOCOLS: result := 'Switching Protocols';
  21726. HTML_SUCCESS: result := 'OK';
  21727. HTML_CREATED: result := 'Created';
  21728. HTML_ACCEPTED: result := 'Accepted';
  21729. HTML_NONAUTHORIZEDINFO: result := 'Non-Authoritative Information';
  21730. HTML_NOCONTENT: result := 'No Content';
  21731. HTML_MULTIPLECHOICES: result := 'Multiple Choices';
  21732. HTML_MOVEDPERMANENTLY: result := 'Moved Permanently';
  21733. HTML_FOUND: result := 'Found';
  21734. HTML_SEEOTHER: result := 'See Other';
  21735. HTML_NOTMODIFIED: result := 'Not Modified';
  21736. HTML_USEPROXY: result := 'Use Proxy';
  21737. HTML_TEMPORARYREDIRECT: result := 'Temporary Redirect';
  21738. HTML_BADREQUEST: result := 'Bad Request';
  21739. HTML_UNAUTHORIZED: result := 'Unauthorized';
  21740. HTML_FORBIDDEN: result := 'Forbidden';
  21741. HTML_NOTFOUND: result := 'Not Found';
  21742. HTML_NOTALLOWED: result := 'Method Not Allowed';
  21743. HTML_NOTACCEPTABLE: result := 'Not Acceptable';
  21744. HTML_PROXYAUTHREQUIRED: result := 'Proxy Authentication Required';
  21745. HTML_TIMEOUT: result := 'Request Timeout';
  21746. HTML_SERVERERROR: result := 'Internal Server Error';
  21747. HTML_BADGATEWAY: result := 'Bad Gateway';
  21748. HTML_GATEWAYTIMEOUT: result := 'Gateway Timeout';
  21749. HTML_UNAVAILABLE: result := 'Service Unavailable';
  21750. HTML_HTTPVERSIONNONSUPPORTED: result := 'HTTP Version Not Supported';
  21751. else result := 'Invalid Request';
  21752. end;
  21753. end;
  21754. function StatusCodeToErrorMsg(Code: integer): RawUTF8;
  21755. begin
  21756. StatusCodeToErrorMsg(Code,result);
  21757. result := FormatUTF8('HTTP Error % - %',[Code,result]);
  21758. end;
  21759. function StatusCodeIsSuccess(Code: integer): boolean;
  21760. begin
  21761. case Code of
  21762. HTML_SUCCESS, HTML_NOCONTENT, HTML_CREATED,
  21763. HTML_NOTMODIFIED, HTML_TEMPORARYREDIRECT:
  21764. result := true;
  21765. else
  21766. result := false;
  21767. end;
  21768. end;
  21769. function StringToMethod(const method: RawUTF8): TSQLURIMethod;
  21770. const NAME: array[mGET..high(TSQLURIMethod)] of string[11] = ( // sorted by occurence
  21771. 'GET','POST','PUT','DELETE','HEAD','BEGIN','END','ABORT','LOCK','UNLOCK','STATE',
  21772. 'OPTIONS','PROPFIND','PROPPATCH','TRACE','COPY','MKCOL','MOVE','PURGE','REPORT',
  21773. 'MKACTIVITY','MKCALENDAR','CHECKOUT','MERGE','NOTIFY','PATCH','SEARCH','CONNECT');
  21774. var URIMethodUp: string[11];
  21775. begin
  21776. if Length(method)<11 then begin
  21777. URIMethodUp[0] := AnsiChar(UpperCopy(@URIMethodUp[1],method)-@URIMethodUp[1]);
  21778. for result := low(NAME) to high(NAME) do
  21779. if URIMethodUp=NAME[result] then
  21780. exit;
  21781. end;
  21782. result := mNone;
  21783. end;
  21784. { ******************* process monitoring / statistics }
  21785. { TSynMonitorUsage }
  21786. function MonitorPropUsageValue(info: PPropInfo): TSynMonitorType;
  21787. var typ: pointer;
  21788. begin
  21789. typ := info^.TypeInfo;
  21790. if typ=TypeInfo(TSynMonitorTotalMicroSec) then
  21791. result := smvMicroSec else
  21792. if typ=TypeInfo(TSynMonitorOneMicroSec) then
  21793. result := smvOneMicroSec else
  21794. if typ=TypeInfo(TSynMonitorTotalBytes) then
  21795. result := smvBytes else
  21796. if typ=TypeInfo(TSynMonitorOneBytes) then
  21797. result := smvOneBytes else
  21798. if typ=TypeInfo(TSynMonitorBytesPerSec) then
  21799. result := smvBytesPerSec else
  21800. if typ=TypeInfo(TSynMonitorCount) then
  21801. result := smvCount else
  21802. if typ=TypeInfo(TSynMonitorCount64) then
  21803. result := smvCount64 else
  21804. result := smvUndefined;
  21805. end;
  21806. function TSynMonitorUsage.Track(Instance: TObject; const Name: RawUTF8): integer;
  21807. procedure ClassTrackProps(ClassType: TClass; var Props: TSynMonitorUsageTrackPropDynArray);
  21808. var i,n: integer;
  21809. nfo: PPropInfo;
  21810. k: TSynMonitorType;
  21811. g: TSynMonitorUsageGranularity;
  21812. p: PSynMonitorUsageTrackProp;
  21813. parent: TClass;
  21814. begin
  21815. n := length(Props);
  21816. while ClassType<>nil do begin
  21817. parent := ClassType.ClassParent;
  21818. for i := 1 to InternalClassPropInfo(ClassType,nfo) do begin
  21819. k := MonitorPropUsageValue(nfo);
  21820. if k<>smvUndefined then begin
  21821. SetLength(Props,n+1);
  21822. p := @Props[n];
  21823. p^.Info := nfo;
  21824. p^.Kind := k;
  21825. ShortStringToAnsi7String(nfo^.Name,p^.Name);
  21826. if (parent<>nil) and (FindPropName(['Bytes','MicroSec'],p^.Name)>=0) then
  21827. p^.Name := RawUTF8(parent.ClassName); // meaningful property name
  21828. for g := mugHour to mugYear do
  21829. SetLength(p^.Values[g],USAGE_VALUE_LEN[g]);
  21830. if k in SYNMONITORVALUE_CUMULATIVE then
  21831. p^.CumulativeLast := nfo^.GetInt64Value(Instance);
  21832. inc(n);
  21833. end;
  21834. nfo := nfo^.Next;
  21835. end;
  21836. ClassType := parent;
  21837. end;
  21838. end;
  21839. var i,n: integer;
  21840. instanceName: RawUTF8;
  21841. begin
  21842. result := -1;
  21843. if Instance=nil then
  21844. exit; // nothing to track
  21845. if (Name='') and Instance.InheritsFrom(TSynMonitor) then
  21846. instanceName := TSynMonitor(Instance).Name else
  21847. instanceName := Name;
  21848. if instanceName='' then
  21849. instanceName := RawUTF8(Instance.ClassName);
  21850. fSafe.Lock;
  21851. try
  21852. n := length(fTracked);
  21853. for i := 0 to n-1 do
  21854. if fTracked[i].Instance=Instance then
  21855. exit else
  21856. if IdemPropNameU(fTracked[i].Name,instanceName) then
  21857. raise ESynException.CreateUTF8('%.Track("%") name already exists',[self,instanceName]);
  21858. SetLength(fTracked,n+1);
  21859. fTracked[n].Instance := Instance;
  21860. fTracked[n].Name := instanceName;
  21861. ClassTrackProps(Instance.ClassType,fTracked[n].Props);
  21862. if fTracked[n].Props=nil then
  21863. // nothing to track
  21864. SetLength(fTracked,n) else begin
  21865. result := n; // returns the index of the added item
  21866. if fPrevious.Value<>0 then
  21867. LoadTrack(fTracked[n]);
  21868. end;
  21869. finally
  21870. fSafe.UnLock;
  21871. end;
  21872. end;
  21873. procedure TSynMonitorUsage.Track(const Instances: array of TSynMonitor);
  21874. var i: integer;
  21875. begin
  21876. if self<>nil then
  21877. for i := 0 to high(Instances) do
  21878. Track(Instances[i],Instances[i].Name);
  21879. end;
  21880. function TSynMonitorUsage.TrackPropLock(Instance: TObject;
  21881. Info: PPropInfo): PSynMonitorUsageTrackProp;
  21882. var i,j: integer;
  21883. begin
  21884. fSafe.Lock;
  21885. for i := 0 to length(fTracked)-1 do
  21886. if fTracked[i].Instance=Instance then
  21887. with fTracked[i] do begin
  21888. for j := 0 to length(Props)-1 do
  21889. if Props[j].Info=Info then begin
  21890. result := @Props[j];
  21891. exit; // returned found entry locked
  21892. end;
  21893. break;
  21894. end;
  21895. fSafe.UnLock;
  21896. result := nil;
  21897. end;
  21898. const
  21899. // maps TTimeLogbits mask
  21900. TL_MASK_SECONDS = pred(1 shl 6);
  21901. TL_MASK_MINUTES = pred(1 shl 12);
  21902. TL_MASK_HOURS = pred(1 shl 17);
  21903. TL_MASK_DAYS = pred(1 shl 22);
  21904. TL_MASK_MONTHS = pred(1 shl 26);
  21905. // truncates a TTimeLogbits value to a granularity
  21906. AS_MINUTES = not TL_MASK_SECONDS;
  21907. AS_HOURS = not TL_MASK_MINUTES;
  21908. AS_DAYS = not TL_MASK_HOURS;
  21909. AS_MONTHS = not TL_MASK_DAYS;
  21910. AS_YEARS = not TL_MASK_MONTHS;
  21911. procedure TSynMonitorUsage.Modified(Instance: TObject);
  21912. begin
  21913. if self<>nil then
  21914. Modified(Instance,[]);
  21915. end;
  21916. procedure TSynMonitorUsage.SetCurrentUTCTime(out minutes: TTimeLogBits);
  21917. begin
  21918. minutes.FromUTCTime;
  21919. end;
  21920. procedure TSynMonitorUsage.Modified(Instance: TObject;
  21921. const PropNames: array of RawUTF8);
  21922. procedure save(const track: TSynMonitorUsageTrack);
  21923. function scope(var prev,current: Int64): TSynMonitorUsageGranularity;
  21924. begin
  21925. if prev and AS_YEARS<>current and AS_YEARS then
  21926. result := mugYear else
  21927. if prev and AS_MONTHS<>current and AS_MONTHS then
  21928. result := mugMonth else
  21929. if prev and AS_DAYS<>current and AS_DAYS then
  21930. result := mugDay else
  21931. if prev and AS_HOURS<>current and AS_HOURS then
  21932. result := mugHour else
  21933. if prev<>current then
  21934. result := mugMinute else
  21935. result := mugUndefined;
  21936. end;
  21937. var j,k,min: integer;
  21938. time: TTimeLogBits;
  21939. v,diff: Int64;
  21940. begin
  21941. SetCurrentUTCTime(time);
  21942. time.Value := time.Value and AS_MINUTES; // save every minute
  21943. if fPrevious.Value<>time.Value then begin
  21944. if fPrevious.Value=0 then // startup?
  21945. Load(time) else
  21946. SavePrevious(scope(fPrevious.Value,time.Value));
  21947. fPrevious.Value := time.Value;
  21948. end;
  21949. min := time.Minute;
  21950. for j := 0 to length(track.Props)-1 do
  21951. with track.Props[j] do
  21952. if (high(PropNames)<0) or (FindPropName(PropNames,Name)>=0) then begin
  21953. v := Info^.GetInt64Value(Instance);
  21954. if Kind in SYNMONITORVALUE_CUMULATIVE then begin
  21955. diff := v-CumulativeLast;
  21956. if diff<>0 then begin
  21957. CumulativeLast := v;
  21958. inc(Values[mugHour][min],diff);
  21959. inc(Values[mugDay][time.Hour],diff); // propagate
  21960. inc(Values[mugMonth][time.Day],diff);
  21961. inc(Values[mugYear][time.Month],diff);
  21962. end;
  21963. end else
  21964. for k := min to 59 do // make instant values continous
  21965. Values[mugHour][min] := v;
  21966. end;
  21967. end;
  21968. var i: integer;
  21969. begin
  21970. if Instance=nil then
  21971. exit;
  21972. fSafe.Lock;
  21973. try
  21974. for i := 0 to length(fTracked)-1 do
  21975. if fTracked[i].Instance=Instance then begin
  21976. save(fTracked[i]);
  21977. exit;
  21978. end;
  21979. if Instance.InheritsFrom(TSynMonitor) and
  21980. (TSynMonitor(Instance).Name<>'') then begin
  21981. i := Track(Instance,TSynMonitor(Instance).Name);
  21982. if i>=0 then
  21983. save(fTracked[i]);
  21984. exit;
  21985. end;
  21986. finally
  21987. fSafe.UnLock;
  21988. end;
  21989. end;
  21990. destructor TSynMonitorUsage.Destroy;
  21991. begin
  21992. SavePrevious(mugUndefined); // save pending values for all granularities
  21993. inherited Destroy;
  21994. end;
  21995. procedure TSynMonitorUsage.SavePrevious(Scope: TSynMonitorUsageGranularity);
  21996. var g: TSynMonitorUsageGranularity;
  21997. id: TSynMonitorUsageID;
  21998. begin
  21999. id.FromTimeLog(fPrevious.Value);
  22000. Save(id,mugHour,Scope); // always save current minutes values
  22001. for g := mugDay to mugYear do
  22002. if (Scope<>mugUndefined) and (g>Scope) then
  22003. break else // mugUndefined from Destroy
  22004. Save(id,g,Scope);
  22005. end;
  22006. procedure TSynMonitorUsage.Save(ID: TSynMonitorUsageID;
  22007. Gran,Scope: TSynMonitorUsageGranularity);
  22008. var t,n,p: Integer;
  22009. track: PSynMonitorUsageTrack;
  22010. data,val: TDocVariantData;
  22011. begin
  22012. TDocVariant.IsOfTypeOrNewFast(fValues[Gran]);
  22013. for t := 0 to length(fTracked)-1 do begin
  22014. track := @fTracked[t];
  22015. n := length(track^.Props);
  22016. data.InitFast(n,dvObject);
  22017. for p := 0 to n-1 do
  22018. with track^.Props[p] do
  22019. if not IsZero(Values[Gran]) then begin
  22020. // save non void values
  22021. val.InitArrayFrom(Values[Gran],JSON_OPTIONS_FAST);
  22022. data.AddValue(Name,Variant(val));
  22023. val.Clear;
  22024. // handle local cache
  22025. if Kind in SYNMONITORVALUE_CUMULATIVE then begin
  22026. if Gran<=Scope then // reset of cumulative values
  22027. FillZero(Values[Gran]);
  22028. end else begin
  22029. if Gran<mugYear then // propagate instant values
  22030. // e.g. Values[mugDay][hour] := Values[mugHour][minute] (=v)
  22031. Values[succ(Gran)][ID.GetTime(Gran)] :=
  22032. Values[Gran][ID.GetTime(pred(Gran))];
  22033. end;
  22034. end;
  22035. _Safe(fValues[Gran]).AddOrUpdateValue(track^.Name,variant(data));
  22036. data.Clear;
  22037. end;
  22038. _Safe(fValues[Gran]).SortByName;
  22039. ID.Truncate(Gran);
  22040. if not SaveDB(ID.Value,fValues[Gran],Gran) then
  22041. fLog.SynLog.Log(sllWarning,'%.Save(ID=%=%,%) failed',
  22042. [ClassType,ID.Value,ID.Text(true),ToText(Gran)^]);
  22043. end;
  22044. procedure TSynMonitorUsage.LoadTrack(var Track: TSynMonitorUsageTrack);
  22045. var p,v: Integer;
  22046. g: TSynMonitorUsageGranularity;
  22047. val,int: PDocVariantData;
  22048. begin // fValues[] variants -> fTracked[].Props[].Values[]
  22049. for g := low(fValues) to high(fValues) do
  22050. with _Safe(fValues[g])^ do begin
  22051. val := GetAsDocVariantSafe(Track.Name);
  22052. if val<>nil then
  22053. for p := 0 to length(Track.Props)-1 do
  22054. with Track.Props[p] do
  22055. if val^.GetAsDocVariant(Name,int) and
  22056. (int^.Count>0) and (int^.Kind=dvArray) then begin
  22057. for v := 0 to length(Values[g])-1 do
  22058. if v<int^.Count then
  22059. Values[g][v] := VariantToInt64Def(int^.Values[v],0);
  22060. end;
  22061. end;
  22062. end;
  22063. function TSynMonitorUsage.Load(const Time: TTimeLogBits): boolean;
  22064. var g: TSynMonitorUsageGranularity;
  22065. id: TSynMonitorUsageID;
  22066. t: integer;
  22067. begin
  22068. // load fValues[] variants
  22069. result := true;
  22070. id.FromTimeLog(Time.Value);
  22071. for g := low(fValues) to high(fValues) do begin
  22072. id.Truncate(g);
  22073. if not LoadDB(id.Value,g,fValues[g]) then
  22074. result := false;
  22075. end;
  22076. // fill fTracked[].Props[].Values[]
  22077. for t := 0 to length(fTracked)-1 do
  22078. LoadTrack(fTracked[t]);
  22079. end;
  22080. { TSynMonitorUsageID }
  22081. procedure TSynMonitorUsageID.From(Y, M, D, H: integer);
  22082. begin
  22083. Value := H+(D-1) shl USAGE_ID_SHIFT[mugDay]+
  22084. (M-1) shl USAGE_ID_SHIFT[mugMonth]+(Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
  22085. end;
  22086. procedure TSynMonitorUsageID.From(Y, M, D: integer);
  22087. begin
  22088. Value := USAGE_ID_HOURMARKER[mugDay]+(D-1) shl USAGE_ID_SHIFT[mugDay]+
  22089. (M-1) shl USAGE_ID_SHIFT[mugMonth]+(Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
  22090. end;
  22091. procedure TSynMonitorUsageID.From(Y, M: integer);
  22092. begin
  22093. Value := USAGE_ID_HOURMARKER[mugMonth]+(M-1) shl USAGE_ID_SHIFT[mugMonth]+
  22094. (Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
  22095. end;
  22096. procedure TSynMonitorUsageID.From(Y: integer);
  22097. begin
  22098. Value := USAGE_ID_HOURMARKER[mugYear]+(Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
  22099. end;
  22100. procedure TSynMonitorUsageID.FromTimeLog(const TimeLog: TTimeLog);
  22101. var bits: TTimeLogBits absolute TimeLog;
  22102. begin
  22103. Value := bits.Hour+(bits.Day-1) shl USAGE_ID_SHIFT[mugDay]+
  22104. (bits.Month-1) shl USAGE_ID_SHIFT[mugMonth]+
  22105. (bits.Year-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
  22106. end;
  22107. procedure TSynMonitorUsageID.FromNowUTC;
  22108. var now: TTimeLogBits;
  22109. begin
  22110. now.FromUTCTime;
  22111. From(now.Value);
  22112. end;
  22113. function TSynMonitorUsageID.GetTime(gran: TSynMonitorUsageGranularity): integer;
  22114. begin
  22115. if not (gran in [low(USAGE_ID_SHIFT)..high(USAGE_ID_SHIFT)]) then
  22116. result := 0 else begin
  22117. result := (Value shr USAGE_ID_SHIFT[gran]) and USAGE_ID_MASK[gran];
  22118. case gran of
  22119. mugYear:
  22120. inc(result,USAGE_ID_YEAROFFSET);
  22121. mugDay, mugMonth:
  22122. inc(result);
  22123. mugHour:
  22124. if cardinal(result)>USAGE_ID_MAX[mugHour] then
  22125. result := 0; // stored fake USAGE_ID_HOURMARKER[mugDay..mugYear] value
  22126. end;
  22127. end;
  22128. end;
  22129. function TSynMonitorUsageID.Granularity: TSynMonitorUsageGranularity;
  22130. var h: integer;
  22131. begin
  22132. h := Value and USAGE_ID_MASK[mugHour];
  22133. if cardinal(h)>USAGE_ID_MAX[mugHour] then begin
  22134. for result := mugDay to mugYear do
  22135. if USAGE_ID_HOURMARKER[result]=h then
  22136. exit;
  22137. result := mugUndefined; // should not happen
  22138. end else
  22139. result := mugHour;
  22140. end;
  22141. procedure TSynMonitorUsageID.Truncate(gran: TSynMonitorUsageGranularity);
  22142. begin
  22143. if gran>mugHour then
  22144. Value := Value and (not USAGE_ID_MASK[mugHour]) or USAGE_ID_HOURMARKER[gran];
  22145. end;
  22146. procedure TSynMonitorUsageID.SetTime(gran: TSynMonitorUsageGranularity; aValue: integer);
  22147. begin
  22148. case gran of
  22149. mugYear: dec(aValue,USAGE_ID_YEAROFFSET);
  22150. mugDay, mugMonth: dec(aValue);
  22151. mugHour: ;
  22152. else raise ERangeError.CreateFmt('SetValue(%s)',[ToText(gran)^]);
  22153. end;
  22154. if cardinal(aValue)>USAGE_ID_MAX[gran] then
  22155. raise ERangeError.CreateFmt('%s should be 0..%d',[ToText(gran)^,USAGE_ID_MAX[gran]]);
  22156. Value := (Value and (not (USAGE_ID_MASK[gran] shl USAGE_ID_SHIFT[gran])))
  22157. or (aValue shl USAGE_ID_SHIFT[gran]);
  22158. end;
  22159. function TSynMonitorUsageID.Text(Expanded: boolean;
  22160. FirstTimeChar: AnsiChar): RawUTF8;
  22161. var bits: TTimeLogBits;
  22162. begin
  22163. bits.Value := ToTimeLog;
  22164. result := bits.Text(Expanded,FirstTimeChar);
  22165. end;
  22166. function TSynMonitorUsageID.ToTimeLog: TTimeLog;
  22167. begin
  22168. PTimeLogBits(@result)^.From(
  22169. GetTime(mugYear),GetTime(mugMonth),GetTime(mugDay),GetTime(mugHour),0,0);
  22170. end;
  22171. { ************ main ORM / SOA classes and types }
  22172. { TSQLTable }
  22173. function TSQLTable.FieldIndex(FieldName: PUTF8Char): integer;
  22174. begin
  22175. if (self<>nil) and (fResults<>nil) and (FieldName<>nil) and (FieldCount>0) then
  22176. if IsRowID(FieldName) then begin // will work for both 'ID' or 'RowID'
  22177. result := fFieldIndexID;
  22178. exit;
  22179. end else
  22180. if FieldCount<4 then begin
  22181. for result := 0 to FieldCount-1 do
  22182. if StrIComp(fResults[result],FieldName)=0 then
  22183. exit;
  22184. end else begin
  22185. if fFieldNameOrder=nil then
  22186. QuickSortIndexedPUTF8Char(fResults,FieldCount,fFieldNameOrder);
  22187. result := FastFindIndexedPUTF8Char(fResults,FieldCount-1,fFieldNameOrder,
  22188. FieldName,@StrIComp);
  22189. exit;
  22190. end;
  22191. result := -1;
  22192. end;
  22193. function TSQLTable.FieldIndex(const FieldName: RawUTF8): integer;
  22194. begin
  22195. result := FieldIndex(Pointer(FieldName));
  22196. end;
  22197. function TSQLTable.FieldIndexExisting(const FieldName: RawUTF8): integer;
  22198. begin
  22199. result := FieldIndex(Pointer(FieldName));
  22200. if result<0 then
  22201. raise ESQLTableException.CreateUTF8('%.FieldIndexExisting("%")',[self,FieldName]);
  22202. end;
  22203. procedure TSQLTable.FieldIndex(const FieldNames: array of RawUTF8;
  22204. const FieldIndexes: array of PInteger);
  22205. var i: integer;
  22206. begin
  22207. if high(FieldNames)<0 then
  22208. exit;
  22209. if high(FieldNames)<>high(FieldIndexes) then
  22210. raise ESQLTableException.CreateUTF8('%.FieldIndex() argument count',[self]);
  22211. for i := 0 to high(FieldNames) do
  22212. if FieldIndexes[i]=nil then
  22213. raise ESQLTableException.CreateUTF8(
  22214. '%.FieldIndex() FieldIndexes["%"]=nil',[self,FieldNames[i]]) else
  22215. FieldIndexes[i]^ := FieldIndex(pointer(FieldNames[i]));
  22216. end;
  22217. procedure TSQLTable.FieldIndexExisting(const FieldNames: array of RawUTF8;
  22218. const FieldIndexes: array of PInteger);
  22219. var i: integer;
  22220. begin
  22221. if high(FieldNames)<0 then
  22222. exit;
  22223. if high(FieldNames)<>high(FieldIndexes) then
  22224. raise ESQLTableException.CreateUTF8('%.FieldIndexExisting() argument count',[self]);
  22225. for i := 0 to high(FieldNames) do
  22226. if FieldIndexes[i]=nil then
  22227. raise ESQLTableException.CreateUTF8(
  22228. '%.FieldIndexExisting() FieldIndexes["%"]=nil',[self,FieldNames[i]]) else
  22229. FieldIndexes[i]^ := FieldIndexExisting(FieldNames[i]);
  22230. end;
  22231. function TSQLTable.FieldNames: TRawUTF8DynArray;
  22232. begin
  22233. if length(fFieldNames)<>fFieldCount then
  22234. InitFieldNames;
  22235. result := fFieldNames;
  22236. end;
  22237. function TSQLTable.FieldValue(const FieldName: RawUTF8; Row: integer): PUTF8Char;
  22238. var Index: integer;
  22239. begin
  22240. Index := FieldIndex(pointer(FieldName));
  22241. if (Index<0) or (cardinal(Row-1)>=cardinal(fRowCount)) then
  22242. result := nil else
  22243. result := fResults[Index+Row*FieldCount];
  22244. end;
  22245. procedure TSQLTable.SortBitsFirst(var Bits);
  22246. var oldIDColumn, oldResults: array of PUTF8Char;
  22247. i, j, nSet, n: integer;
  22248. R: PPUTF8Char;
  22249. begin
  22250. if fIDColumn<>nil then begin
  22251. n := length(fIDColumn);
  22252. SetLength(oldIDColumn,n);
  22253. MoveFast(fIDColumn[0],oldIDColumn[0],n*sizeof(PUTF8Char));
  22254. end;
  22255. i := (fRowCount+1)*FieldCount;
  22256. SetLength(oldResults,i);
  22257. MoveFast(fResults[0],oldResults[0],i*sizeof(PUTF8Char));
  22258. // put marked IDs first
  22259. n := 1; // copy row data (first row=0 i.e. idents is left as it is)
  22260. R := @fResults[FieldCount];
  22261. j := FieldCount;
  22262. for i := 1 to fRowCount do begin
  22263. if GetBit(Bits,i-1) then begin
  22264. if fIDColumn<>nil then
  22265. fIDColumn[n] := oldIDColumn[i];
  22266. MoveFast(oldResults[j],R^,FieldCount*sizeof(PUTF8Char));
  22267. inc(n);
  22268. inc(R,FieldCount);
  22269. end;
  22270. inc(j,FieldCount);
  22271. end;
  22272. nSet := n-1;
  22273. // put unmarked IDs
  22274. j := FieldCount;
  22275. for i := 1 to fRowCount do begin
  22276. if not GetBit(Bits,i-1) then begin
  22277. if fIDColumn<>nil then
  22278. fIDColumn[n] := oldIDColumn[i];
  22279. MoveFast(oldResults[j],R^,FieldCount*sizeof(PUTF8Char));
  22280. inc(n);
  22281. inc(R,FieldCount);
  22282. end;
  22283. inc(j,FieldCount);
  22284. end;
  22285. assert(n-1=fRowCount);
  22286. // recalcultate Bits[]
  22287. FillcharFast(Bits,(fRowCount shr 3)+1,0);
  22288. for i := 0 to nSet-1 do
  22289. SetBit(Bits,i); // slow but accurate
  22290. {$ifdef FPC}
  22291. Finalize(oldIDColumn); // alf: to circumvent FPC issues
  22292. Finalize(oldResults);
  22293. {$endif}
  22294. end;
  22295. function TSQLTable.IDColumnHide: boolean;
  22296. var FID,R,F: integer;
  22297. S,D1,D2: PPUTF8Char;
  22298. begin
  22299. // 1. check if possible
  22300. result := false;
  22301. if (self=nil) or Assigned(fIDColumn) or (FieldCount<=1) then
  22302. exit; // already hidden or not possible
  22303. FID := fFieldIndexID;
  22304. if FID<0 then
  22305. exit; // no 'ID' field
  22306. // 2. alloc new arrays of PUTF8Char
  22307. dec(fFieldCount);
  22308. R := fRowCount+1;
  22309. SetLength(fIDColumn,R); // will contain the ID column data
  22310. SetLength(fNotIDColumn,R*FieldCount); // will be the new fResults[]
  22311. // 3. copy fResults[] into new arrays
  22312. S := @fResults[0];
  22313. D1 := @fNotIDColumn[0];
  22314. D2 := @fIDColumn[0];
  22315. for R := 0 to fRowCount do
  22316. for F := 0 to FieldCount do begin // we have FieldCount := FieldCount-1
  22317. if F<>FID then begin
  22318. D1^ := S^; // copy not ID column into fNotIDColumn[]
  22319. inc(D1);
  22320. end else begin
  22321. D2^ := S^; // copy ID column into fIDColumn[]
  22322. inc(D2);
  22323. end;
  22324. inc(S);
  22325. end;
  22326. // 4. TSQLTable data now points to new values without ID field
  22327. result := true;
  22328. fResults := @fNotIDColumn[0];
  22329. end;
  22330. function TSQLTable.IDColumnHiddenValue(Row: integer): TID;
  22331. begin
  22332. if (self=nil) or (fResults=nil) or (Row<=0) or (Row>fRowCount) then
  22333. result := 0 else
  22334. if Assigned(fIDColumn) then // get hidden ID column UTF-8 content
  22335. SetID(fIDColumn[Row],result) else
  22336. if fFieldIndexID>=0 then // get ID column field index
  22337. SetID(fResults[Row*FieldCount+fFieldIndexID],result) else
  22338. result := 0;
  22339. end;
  22340. procedure TSQLTable.IDArrayFromBits(const Bits; var IDs: TIDDynArray);
  22341. var n, i, FID: integer;
  22342. begin
  22343. if not Assigned(fIDColumn) then begin
  22344. FID := fFieldIndexID; // get ID column field index
  22345. if FID<0 then
  22346. exit;
  22347. end else
  22348. FID := 0; // make compiler happy
  22349. n := GetBitsCount(Bits,fRowCount);
  22350. if n=fRowCount then begin
  22351. IDColumnHiddenValues(IDs); // all selected -> direct get all IDs
  22352. exit;
  22353. end;
  22354. SetLength(IDs,n);
  22355. if n=0 then
  22356. exit;
  22357. n := 0;
  22358. if Assigned(fIDColumn) then begin
  22359. for i := 1 to fRowCount do
  22360. if GetBit(Bits,i-1) then begin
  22361. IDs[n] := GetInt64(fIDColumn[i]); // get hidden ID column UTF-8 content
  22362. inc(n);
  22363. end;
  22364. end else begin
  22365. inc(FID,FieldCount); // [i*FieldCount+FID] = [(i+1)*FieldCount+FID] below
  22366. for i := 0 to fRowCount-1 do
  22367. if GetBit(Bits,i) then begin
  22368. IDs[n] := GetInt64(fResults[i*FieldCount+FID]); // get ID column UTF-8 content
  22369. inc(n);
  22370. end;
  22371. end;
  22372. end;
  22373. procedure TSQLTable.IDColumnHiddenValues(var IDs: TIDDynArray);
  22374. var n, i, FID: integer;
  22375. U: PPUTF8Char;
  22376. begin
  22377. n := fRowCount;
  22378. if not Assigned(fIDColumn) then begin
  22379. FID := fFieldIndexID; // get ID column field index
  22380. if FID<0 then
  22381. n := 0;
  22382. end else
  22383. FID := 0;
  22384. SetLength(IDs,n);
  22385. if n=0 then
  22386. exit;
  22387. if Assigned(fIDColumn) then begin
  22388. for i := 1 to fRowCount do
  22389. IDs[i-1] := GetInt64(fIDColumn[i]); // get hidden ID column UTF-8 content
  22390. end else begin
  22391. U := @fResults[FID+FieldCount]; // U^ = ID column UTF-8 content
  22392. for i := 0 to fRowCount-1 do begin
  22393. IDs[i] := GetInt64(U^);
  22394. inc(U,FieldCount);
  22395. end;
  22396. end;
  22397. end;
  22398. procedure TSQLTable.IDArrayToBits(var Bits; var IDs: TIDDynArray);
  22399. var i,FID: integer;
  22400. U: PPUTF8Char;
  22401. ID: Pointer;
  22402. IDn: integer;
  22403. // AllID: : TIDDynArray;
  22404. begin
  22405. if length(IDs)=RowCount then begin
  22406. FillcharFast(Bits,(RowCount shr 3)+1,255); // all selected -> all bits set to 1
  22407. exit;
  22408. end;
  22409. FillcharFast(Bits,(RowCount shr 3)+1,0);
  22410. if IDs=nil then
  22411. exit; // no selected -> all bits left to 0
  22412. // we sort IDs to use FastFindIntegerSorted() and its fast binary search
  22413. ID := @IDs[0];
  22414. IDn := high(IDs);
  22415. QuickSortInt64(ID,0,IDn);
  22416. if not Assigned(fIDColumn) then begin
  22417. FID := fFieldIndexID; // get ID column field index
  22418. if FID<0 then
  22419. exit; // no ID column -> unable to get bit index
  22420. end else
  22421. FID := 0; // make compiler happy
  22422. if Assigned(fIDColumn) then begin
  22423. for i := 1 to RowCount do
  22424. if FastFindInt64Sorted(ID,IDn,GetInt64(fIDColumn[i]))>=0 then
  22425. SetBit(Bits,i-1);
  22426. end else begin
  22427. U := @fResults[FID+FieldCount]; // U^ = ID column UTF-8 content
  22428. for i := 0 to RowCount-1 do begin
  22429. if FastFindInt64Sorted(ID,IDn,GetInt64(U^))>=0 then
  22430. SetBit(Bits,i);
  22431. inc(U,FieldCount);
  22432. end;
  22433. end;
  22434. { // debugg:
  22435. IDArrayFromBits(Bits,AllID);
  22436. assert(length(AllID)=length(IDs));
  22437. QuickSortInteger(@AllID[0],0,high(AllID));
  22438. QuickSortInteger(@IDs[0],0,high(IDs));
  22439. assert(comparemem(@AllID[0],@IDs[0],length(AllID)*sizeof(TID))); }
  22440. end;
  22441. function TSQLTable.RowFromID(aID: TID): integer;
  22442. var ID: RawUTF8;
  22443. FID: integer;
  22444. U: PPUTF8Char;
  22445. begin
  22446. if self=nil then begin
  22447. result := -1;
  22448. exit;
  22449. end;
  22450. if (fResults<>nil) and (aID>0) then begin
  22451. // search aID as UTF-8 in fIDColumn[] or fResults[]
  22452. Int64ToUtf8(aID,ID);
  22453. if Assigned(fIDColumn) then begin // get hidden ID column UTF-8 content
  22454. for result := 1 to fRowCount do
  22455. if StrComp(fIDColumn[result],pointer(ID))=0 then
  22456. exit;
  22457. end else begin
  22458. FID := fFieldIndexID; // get ID column field index
  22459. if FID>=0 then begin
  22460. U := @fResults[FID+FieldCount]; // U^ = ID column UTF-8 content
  22461. for result := 1 to fRowCount do
  22462. if StrComp(U^,pointer(ID))=0 then
  22463. exit else
  22464. inc(U,FieldCount);
  22465. end;
  22466. end;
  22467. end;
  22468. result := fRowCount; // not found -> return last row index
  22469. end;
  22470. procedure TSQLTable.DeleteRow(Row: integer);
  22471. begin
  22472. if (self=nil) or (Row<1) or (Row>fRowCount) then
  22473. exit; // out of range
  22474. if Assigned(fIDColumn) then
  22475. if Row<fRowCount then
  22476. MoveFast(fIDColumn[Row+1],fIDColumn[Row],(fRowCount-Row)*sizeof(PUTF8Char));
  22477. if Row<fRowCount then begin
  22478. Row := Row*FieldCount; // convert row index into position in fResults[]
  22479. MoveFast(fResults[Row+FieldCount],fResults[Row],(fRowCount*FieldCount-Row)*sizeof(pointer));
  22480. end;
  22481. dec(fRowCount);
  22482. end;
  22483. procedure TSQLTable.InitFieldNames;
  22484. var f: integer;
  22485. P: PUTF8Char;
  22486. begin
  22487. SetLength(fFieldNames,fFieldCount); // share one TRawUTF8DynArray
  22488. for f := 0 to fFieldCount-1 do begin
  22489. P := Get(0,f);
  22490. if IsRowID(P) then // normalize RowID field name to ID
  22491. fFieldNames[f] := 'ID' else
  22492. fFieldNames[f] := P;
  22493. end;
  22494. end;
  22495. {$ifndef NOVARIANTS}
  22496. var
  22497. SQLTableRowVariantType: TCustomVariantType = nil;
  22498. procedure TSQLTable.GetAsVariant(row,field: integer; out value: variant;
  22499. expandTimeLogAsText,expandEnumsAsText,expandHugeIDAsUniqueIdentifier: boolean;
  22500. options: TDocVariantOptions);
  22501. const JAN2015_UNIX = 1420070400;
  22502. var t: TTimeLogBits;
  22503. id: TSynUniqueIdentifierBits;
  22504. V: PUtf8Char;
  22505. enum,err: integer;
  22506. begin
  22507. if (self=nil) or (row<1) or (row>fRowCount) or
  22508. (cardinal(field)>=cardinal(fFieldCount)) then
  22509. exit; // out of range
  22510. if not Assigned(fFieldType) then
  22511. InitFieldTypes;
  22512. V := fResults[row*fFieldCount+field];
  22513. with fFieldType[field] do
  22514. if expandHugeIDAsUniqueIdentifier and (field=fFieldIndexID) then begin
  22515. SetInt64(V,PInt64(@id)^);
  22516. if id.CreateTimeUnix>JAN2015_UNIX then
  22517. value := id.AsVariant else
  22518. value := id.Value;
  22519. end else begin
  22520. if expandEnumsAsText and (ContentType=sftEnumerate) then begin
  22521. enum := GetInteger(V,err);
  22522. if (err=0) and (ContentTypeInfo<>nil) then begin
  22523. value := PEnumType(ContentTypeInfo)^.GetEnumNameOrd(enum)^;
  22524. exit;
  22525. end;
  22526. end else
  22527. if expandTimeLogAsText and (ContentType in [sftTimeLog,sftModTime,sftCreateTime]) then begin
  22528. SetInt64(V,t.Value);
  22529. value := _ObjFast(['Time',t.Text(true),'Value',PInt64(@t)^]);
  22530. exit;
  22531. end;
  22532. ValueVarToVariant(V,ContentType,TVarData(value),true,ContentTypeInfo,options);
  22533. end;
  22534. end;
  22535. procedure TSQLTable.ToDocVariant(Row: integer; out doc: variant;
  22536. options: TDocVariantOptions; expandTimeLogAsText,expandEnumsAsText,
  22537. expandHugeIDAsUniqueIdentifier: boolean);
  22538. var Values: TVariantDynArray;
  22539. f: integer;
  22540. begin
  22541. if (self=nil) or (Row<1) or (Row>fRowCount) then
  22542. exit; // out of range
  22543. SetLength(Values,fFieldCount);
  22544. for f := 0 to fFieldCount-1 do
  22545. GetAsVariant(Row,f,Values[f],expandTimeLogAsText,expandEnumsAsText,
  22546. expandHugeIDAsUniqueIdentifier,options);
  22547. if length(fFieldNames)<>fFieldCount then
  22548. InitFieldNames;
  22549. TDocVariantData(doc).InitObjectFromVariants(fFieldNames,Values,options);
  22550. end;
  22551. procedure TSQLTable.ToDocVariant(out docs: TVariantDynArray; readonly: boolean);
  22552. var r: integer;
  22553. begin
  22554. if (self=nil) or (fRowCount=0) then
  22555. exit;
  22556. SetLength(docs,fRowCount);
  22557. if readonly then begin
  22558. if SQLTableRowVariantType=nil then
  22559. SQLTableRowVariantType := SynRegisterCustomVariantType(TSQLTableRowVariant);
  22560. for r := 0 to fRowCount-1 do
  22561. with TSQLTableRowVariantData(docs[r]) do begin
  22562. VType := SQLTableRowVariantType.VarType;
  22563. VTable := self;
  22564. VRow := r+1;
  22565. end;
  22566. end else
  22567. for r := 0 to fRowCount-1 do
  22568. ToDocVariant(r+1,docs[r]);
  22569. end;
  22570. procedure TSQLTable.ToDocVariant(out docarray: variant; readonly: boolean);
  22571. var Values: TVariantDynArray;
  22572. begin
  22573. ToDocVariant(Values,readonly);
  22574. TDocVariantData(docarray).InitArrayFromVariants(Values,JSON_OPTIONS_FAST);
  22575. end;
  22576. {$endif NOVARIANTS}
  22577. procedure TSQLTable.DeleteColumnValues(Field: integer);
  22578. var i: integer;
  22579. U: PPUTF8Char;
  22580. begin
  22581. if cardinal(Field)>=cardinal(FieldCount) then
  22582. exit; // out of range
  22583. U := @fResults[Field+FieldCount]; // U^ = column UTF-8 content for this field
  22584. for i := 1 to fRowCount do begin
  22585. U^ := nil; // just void UTF-8 content text
  22586. inc(U,FieldCount);
  22587. end;
  22588. end;
  22589. function TSQLTable.GetQueryTableNameFromSQL: RawUTF8;
  22590. begin
  22591. if (fQueryTableNameFromSQL='') and (fQuerySQL<>'') then
  22592. fQueryTableNameFromSQL := GetTableNameFromSQLSelect(fQuerySQL,true);
  22593. result := fQueryTableNameFromSQL;
  22594. end;
  22595. function TSQLTable.FieldPropFromTables(const PropName: RawUTF8;
  22596. out PropInfo: TSQLPropInfo; out TableIndex: integer): TSQLFieldType;
  22597. procedure SearchInQueryTables(aPropName: PUTF8Char; aTableIndex: integer);
  22598. begin
  22599. if IsRowID(aPropName) then begin
  22600. result := sftInteger;
  22601. PropInfo := nil;
  22602. TableIndex := aTableIndex;
  22603. exit;
  22604. end else
  22605. if fQueryTables[aTableIndex]<>nil then begin
  22606. PropInfo := fQueryTables[aTableIndex].RecordProps.Fields.ByName(aPropName);
  22607. if PropInfo<>nil then begin
  22608. result := PropInfo.SQLFieldTypeStored;
  22609. if result<>sftUnknown then
  22610. TableIndex := aTableIndex;
  22611. exit;
  22612. end;
  22613. result := sftUnknown;
  22614. end;
  22615. end;
  22616. var i,t: integer;
  22617. begin
  22618. TableIndex := -1;
  22619. if fQueryTableIndexFromSQL=-2 then begin
  22620. fQueryTableIndexFromSQL := -1;
  22621. if (fQueryTables<>nil) and (QueryTableNameFromSQL<>'') then
  22622. for i := 0 to length(fQueryTables)-1 do
  22623. if IdemPropNameU(fQueryTables[i].SQLTableName,fQueryTableNameFromSQL) then begin
  22624. fQueryTableIndexFromSQL := i;
  22625. break;
  22626. end;
  22627. end;
  22628. if fQueryTableIndexFromSQL>=0 then begin
  22629. SearchInQueryTables(pointer(PropName),fQueryTableIndexFromSQL);
  22630. if result<>sftUnknown then
  22631. exit;
  22632. end;
  22633. if length(fQueryTables)=1 then
  22634. SearchInQueryTables(pointer(PropName),0)
  22635. else begin
  22636. i := PosEx('.',PropName)-1;
  22637. if i<0 then // no 'ClassName.PropertyName' format: find first exact property name
  22638. for t := 0 to high(fQueryTables) do begin
  22639. SearchInQueryTables(pointer(PropName),t);
  22640. if result<>sftUnknown then
  22641. exit;
  22642. end
  22643. else // handle property names as 'ClassName.PropertyName'
  22644. for t := 0 to high(fQueryTables) do
  22645. if fQueryTables[t]<>nil then // avoid GPF
  22646. if IdemPropNameU(fQueryTables[t].RecordProps.SQLTableName,pointer(PropName),i) then begin
  22647. SearchInQueryTables(@PropName[i+2],t);
  22648. exit;
  22649. end;
  22650. result := sftUnknown;
  22651. end;
  22652. end;
  22653. procedure TSQLTable.SetFieldType(Field: integer; FieldType: TSQLFieldType;
  22654. FieldTypeInfo: pointer; FieldSize,FieldTableIndex: integer);
  22655. begin
  22656. if (self=nil) or (cardinal(Field)>=cardinal(FieldCount)) then
  22657. exit;
  22658. if fFieldType=nil then
  22659. InitFieldTypes;
  22660. with fFieldType[Field] do begin
  22661. ContentType := FieldType;
  22662. ContentSize := FieldSize;
  22663. ContentTypeInfo := nil;
  22664. if FieldTypeInfo<>nil then
  22665. case FieldType of
  22666. sftEnumerate:
  22667. if (PTypeInfo(FieldTypeInfo)^.Kind=tkEnumeration) then
  22668. ContentTypeInfo := PTypeInfo(FieldTypeInfo)^.EnumBaseType;
  22669. sftSet:
  22670. if (PTypeInfo(FieldTypeInfo)^.Kind=tkSet) then
  22671. ContentTypeInfo := PTypeInfo(FieldTypeInfo)^.SetEnumType;
  22672. sftBlobDynArray:
  22673. ContentTypeInfo := FieldTypeInfo;
  22674. sftNullable: begin
  22675. ContentTypeInfo := FieldTypeInfo;
  22676. ContentType := NullableTypeToSQLFieldType(FieldTypeInfo);
  22677. if ContentType=sftUnknown then
  22678. ContentType := sftNullable;
  22679. end;
  22680. end;
  22681. TableIndex := FieldTableIndex;
  22682. end;
  22683. end;
  22684. procedure TSQLTable.SetFieldType(const FieldName: RawUTF8; FieldType: TSQLFieldType;
  22685. FieldTypeInfo: pointer=nil; FieldSize: integer=-1);
  22686. begin
  22687. SetFieldType(FieldIndex(FieldName),FieldType,FieldTypeInfo,FieldSize);
  22688. end;
  22689. function TSQLTable.GetRowCount: integer;
  22690. begin
  22691. if self=nil then
  22692. result := 0 else
  22693. result := fRowCount;
  22694. end;
  22695. procedure TSQLTable.InitFieldTypes;
  22696. var f,i,len: integer;
  22697. FieldType: TSQLFieldType;
  22698. FieldTypeInfo: pointer;
  22699. FieldPropInfo: TSQLPropInfo;
  22700. FieldSize,FieldTableIndex: integer;
  22701. U: PPUTF8Char;
  22702. tlog: TTimeLog;
  22703. begin
  22704. if Assigned(fQueryColumnTypes) and (FieldCount<>length(fQueryColumnTypes)) then
  22705. raise ESQLTableException.CreateUTF8('%.CreateWithColumnTypes() called with % '+
  22706. 'column types, whereas the result has % columns',
  22707. [self,length(fQueryColumnTypes),FieldCount]);
  22708. SetLength(fFieldType,FieldCount);
  22709. for f := 0 to FieldCount-1 do begin
  22710. FieldPropInfo := nil;
  22711. FieldTypeInfo := nil;
  22712. FieldSize := -1;
  22713. FieldTableIndex := -1;
  22714. // init fFieldType[] from fQueryTables/fQueryColumnTypes[]
  22715. if Assigned(fQueryColumnTypes) then
  22716. FieldType := fQueryColumnTypes[f] else
  22717. if Assigned(QueryTables) then begin // retrieve column info from field name
  22718. FieldType := FieldPropFromTables(fResults[f],FieldPropInfo,FieldTableIndex);
  22719. if FieldPropInfo<>nil then begin
  22720. if FieldPropInfo.InheritsFrom(TSQLPropInfoRTTI) then
  22721. FieldTypeInfo := TSQLPropInfoRTTI(FieldPropInfo).PropType;
  22722. FieldSize := FieldPropInfo.FieldWidth;
  22723. end;
  22724. end else
  22725. FieldType := sftUnknown;
  22726. if FieldType=sftUnknown then
  22727. // not found in fQueryTables/fQueryColumnTypes[]: guess from content
  22728. if IsRowID(fResults[f]) then
  22729. FieldType := sftInteger else
  22730. if f in fFieldParsedAsString then begin
  22731. // the parser identified string values -> check if was sftDateTime
  22732. FieldType := sftUTF8Text;
  22733. U := @fResults[FieldCount+f];
  22734. for i := 1 to fRowCount do
  22735. if U^=nil then // search for a non void column
  22736. inc(U,FieldCount) else begin
  22737. len := StrLen(U^);
  22738. tlog := Iso8601ToTimeLogPUTF8Char(U^,len);
  22739. if tlog<>0 then
  22740. if (len in [8,10]) and (cardinal(tlog shr 26)-1800<300) then
  22741. FieldType := sftDateTime else // e.g. YYYYMMDD date (Y=1800..2100)
  22742. if len>=15 then
  22743. FieldType := sftDateTime; // e.g. YYYYMMDDThhmmss date/time value
  22744. break;
  22745. end;
  22746. end else begin
  22747. U := @fResults[FieldCount+f];
  22748. for i := 1 to fRowCount do begin
  22749. FieldType := UTF8ContentNumberType(U^);
  22750. inc(U,FieldCount);
  22751. if FieldType=sftUnknown then
  22752. continue else // null -> search for a non void column
  22753. if FieldType=sftInteger then // may be a floating point with no decimal
  22754. if FieldTypeIntegerDetectionOnAllRows then
  22755. continue else
  22756. // we only checked the first field -> best guess...
  22757. FieldType := sftCurrency;
  22758. break; // found a non-integer content (e.g. sftFloat/sftUtf8Text)
  22759. end;
  22760. end;
  22761. SetFieldType(f,FieldType,FieldTypeInfo,FieldSize,FieldTableIndex);
  22762. end;
  22763. end;
  22764. function TSQLTable.FieldType(Field: integer): TSQLFieldType;
  22765. begin
  22766. if (self<>nil) and (cardinal(Field)<cardinal(FieldCount)) then begin
  22767. if not Assigned(fFieldType) then
  22768. InitFieldTypes;
  22769. result := fFieldType[Field].ContentType;
  22770. end else
  22771. result := sftUnknown;
  22772. end;
  22773. function TSQLTable.FieldType(Field: integer; OutFieldTypeInfo: PPointer): TSQLFieldType;
  22774. begin
  22775. if (self<>nil) and (cardinal(Field)<cardinal(FieldCount)) then begin
  22776. if not Assigned(fFieldType) then
  22777. InitFieldTypes;
  22778. result := fFieldType[Field].ContentType;
  22779. if OutFieldTypeInfo<>nil then
  22780. OutFieldTypeInfo^ := fFieldType[Field].ContentTypeInfo;
  22781. end else
  22782. result := sftUnknown;
  22783. end;
  22784. function TSQLTable.Get(Row, Field: integer): PUTF8Char;
  22785. begin
  22786. if (self=nil) or (fResults=nil) or (cardinal(Row)>cardinal(fRowCount)) or
  22787. (cardinal(Field)>=cardinal(FieldCount)) then // cardinal() -> test <0
  22788. result := nil else
  22789. result := fResults[Row*FieldCount+Field];
  22790. end;
  22791. function TSQLTable.GetU(Row,Field: integer): RawUTF8;
  22792. var P: PUTF8Char;
  22793. begin
  22794. if (self=nil) or (fResults=nil) or (cardinal(Row)>cardinal(fRowCount)) or
  22795. (cardinal(Field)>=cardinal(FieldCount)) then // cardinal() -> test <0
  22796. result := '' else begin
  22797. P := fResults[Row*FieldCount+Field];
  22798. SetString(Result,PAnsiChar(P),StrLen(P));
  22799. end;
  22800. end;
  22801. function TSQLTable.Get(Row: integer; const FieldName: RawUTF8): PUTF8Char;
  22802. begin
  22803. result := Get(Row,FieldIndex(FieldName));
  22804. end;
  22805. function TSQLTable.GetU(Row: integer; const FieldName: RawUTF8): RawUTF8;
  22806. begin
  22807. result := GetU(Row,FieldIndex(FieldName));
  22808. end;
  22809. function TSQLTable.GetA(Row, Field: integer): WinAnsiString;
  22810. begin
  22811. result := Utf8ToWinAnsi(Get(Row,Field));
  22812. end;
  22813. function TSQLTable.GetAsInteger(Row, Field: integer): integer;
  22814. begin
  22815. result := GetInteger(Get(Row,Field));
  22816. end;
  22817. function TSQLTable.GetAsInteger(Row: integer; const FieldName: RawUTF8): integer;
  22818. begin
  22819. result := GetInteger(Get(Row,FieldIndex(FieldName)));
  22820. end;
  22821. function TSQLTable.GetAsInt64(Row, Field: integer): Int64;
  22822. begin
  22823. SetInt64(Get(Row,Field),result);
  22824. end;
  22825. function TSQLTable.GetAsInt64(Row: integer; const FieldName: RawUTF8): Int64;
  22826. begin
  22827. SetInt64(Get(Row,FieldIndex(FieldName)),result);
  22828. end;
  22829. function TSQLTable.GetAsFloat(Row,Field: integer): TSynExtended;
  22830. begin
  22831. result := GetExtended(Get(Row,Field));
  22832. end;
  22833. function TSQLTable.GetAsFloat(Row: integer; const FieldName: RawUTF8): TSynExtended;
  22834. begin
  22835. result := GetExtended(Get(Row,FieldIndex(FieldName)));
  22836. end;
  22837. function TSQLTable.GetAsCurrency(Row,Field: integer): currency;
  22838. begin
  22839. result := StrToCurrency(Get(Row,Field));
  22840. end;
  22841. function TSQLTable.GetAsCurrency(Row: integer; const FieldName: RawUTF8): currency;
  22842. begin
  22843. result := StrToCurrency(Get(Row,FieldIndex(FieldName)));
  22844. end;
  22845. function TSQLTable.GetAsDateTime(Row,Field: integer): TDateTime;
  22846. var P: PUTF8Char;
  22847. begin
  22848. result := 0;
  22849. if Row=0 then
  22850. exit; // header
  22851. P := Get(Row,Field);
  22852. if P=nil then
  22853. exit;
  22854. case FieldType(Field) of
  22855. sftCurrency,sftFloat:
  22856. result := GetExtended(P);
  22857. sftInteger, // TSQLTable.InitFieldTypes may have recognized an integer
  22858. sftTimeLog, sftModTime, sftCreateTime:
  22859. result := TimeLogToDateTime(GetInt64(P));
  22860. else // sftDateTime and any other kind will try from ISO-8601 text
  22861. result := Iso8601ToDateTimePUTF8Char(P);
  22862. end;
  22863. end;
  22864. function TSQLTable.GetAsDateTime(Row: integer; const FieldName: RawUTF8): TDateTime;
  22865. begin
  22866. result := GetAsDateTime(Row,FieldIndex(FieldName));
  22867. end;
  22868. function TSQLTable.GetS(Row, Field: integer): shortstring;
  22869. begin
  22870. UTF8ToShortString(result,Get(Row,Field));
  22871. end;
  22872. function TSQLTable.GetString(Row, Field: integer): string;
  22873. var U: PUTF8Char;
  22874. begin
  22875. U := Get(Row,Field);
  22876. if U=nil then
  22877. result := '' else
  22878. {$ifdef UNICODE}
  22879. UTF8DecodeToUnicodeString(U,StrLen(U),result);
  22880. {$else}
  22881. CurrentAnsiConvert.UTF8BufferToAnsi(U,StrLen(U),RawByteString(result));
  22882. {$endif}
  22883. end;
  22884. function TSQLTable.GetSynUnicode(Row,Field: integer): SynUnicode;
  22885. var U: PUTF8Char;
  22886. begin
  22887. result := '';
  22888. U := Get(Row,Field);
  22889. if U<>nil then
  22890. UTF8ToSynUnicode(U,StrLen(U),result);
  22891. end;
  22892. function TSQLTable.GetCaption(Row, Field: integer): string;
  22893. begin
  22894. GetCaptionFromPCharLen(Get(Row,Field),result);
  22895. end;
  22896. function BlobToTSQLRawBlob(P: PUTF8Char): TSQLRawBlob;
  22897. var Len, LenHex: integer;
  22898. begin
  22899. result := '';
  22900. if P=nil then
  22901. exit;
  22902. Len := StrLen(P);
  22903. if Len=0 then
  22904. exit;
  22905. if (P[0] in ['x','X']) and (P[1]='''') and (P[Len-1]='''') then begin
  22906. // BLOB literals are string literals containing hexadecimal data and
  22907. // preceded by a single "x" or "X" character. For example: X'53514C697465'
  22908. LenHex := (Len-3) shr 1;
  22909. SetLength(result,LenHex);
  22910. if SynCommons.HexToBin(@P[2],pointer(result),LenHex) then
  22911. exit; // valid hexa data
  22912. end else
  22913. if (PInteger(P)^ and $00ffffff=JSON_BASE64_MAGIC) and IsBase64(@P[3],Len-3) then begin
  22914. // Base-64 encoded content ('\uFFF0base64encodedbinary')
  22915. result := Base64ToBin(@P[3],Len-3);
  22916. exit;
  22917. end;
  22918. // TEXT format
  22919. SetString(result,PAnsiChar(P),Len);
  22920. end;
  22921. function BlobToTSQLRawBlob(const Blob: RawByteString): TSQLRawBlob;
  22922. var Len, LenHex: integer;
  22923. P: PUTF8Char;
  22924. begin
  22925. result := '';
  22926. if Blob='' then
  22927. exit;
  22928. Len := length(Blob);
  22929. P := pointer(Blob);
  22930. if (P[0] in ['x','X']) and (P[1]='''') and (P[Len-1]='''') then begin
  22931. // BLOB literals are string literals containing hexadecimal data and
  22932. // preceded by a single "x" or "X" character. For example: X'53514C697465'
  22933. LenHex := (Len-3) shr 1;
  22934. SetLength(result,LenHex);
  22935. if SynCommons.HexToBin(@P[2],pointer(result),LenHex) then
  22936. exit; // valid hexa data
  22937. end else
  22938. if (PInteger(P)^ and $00ffffff=JSON_BASE64_MAGIC) and IsBase64(@P[3],Len-3) then begin
  22939. // Base-64 encoded content ('\uFFF0base64encodedbinary')
  22940. result := Base64ToBin(@P[3],Len-3);
  22941. exit;
  22942. end;
  22943. // TEXT format
  22944. result := Blob;
  22945. end;
  22946. function BlobToStream(P: PUTF8Char): TStream;
  22947. begin
  22948. Result := TRawByteStringStream.Create(BlobToTSQLRawBlob(P));
  22949. end;
  22950. function BlobToBytes(P: PUTF8Char): TBytes;
  22951. var Len, LenResult: integer;
  22952. begin
  22953. result := nil;
  22954. Len := StrLen(P);
  22955. if Len=0 then
  22956. exit;
  22957. if (P[0] in ['x','X']) and (P[1]='''') and (P[Len-1]='''') then begin
  22958. // BLOB literals format
  22959. LenResult := (Len-3)shr 1;
  22960. SetLength(Result,LenResult);
  22961. if SynCommons.HexToBin(@P[2],pointer(Result),LenResult) then
  22962. exit; // valid hexa data
  22963. end else
  22964. if (PInteger(P)^ and $00ffffff=JSON_BASE64_MAGIC) and IsBase64(@P[3],Len-3) then begin
  22965. // Base-64 encoded content ('\uFFF0base64encodedbinary')
  22966. inc(P,3);
  22967. dec(Len,3);
  22968. LenResult := Base64ToBinLength(pointer(P),len);
  22969. SetLength(Result,LenResult);
  22970. if LenResult>0 then
  22971. Base64Decode(pointer(P),pointer(Result),Len shr 2);
  22972. exit;
  22973. end;
  22974. // TEXT format
  22975. SetLength(Result,Len);
  22976. MoveFast(P^,pointer(Result)^,Len);
  22977. end;
  22978. function TSQLRawBlobToBlob(const RawBlob: TSQLRawBlob): RawUTF8;
  22979. // BLOB literals are string literals containing hexadecimal data and
  22980. // preceded by a single "x" or "X" character. For example: X'53514C697465'
  22981. begin
  22982. result := TSQLRawBlobToBlob(pointer(RawBlob),length(RawBlob));
  22983. end;
  22984. function TSQLRawBlobToBlob(RawBlob: pointer; RawBlobLength: integer): RawUTF8; overload;
  22985. // BLOB literals are string literals containing hexadecimal data and
  22986. // preceded by a single "x" or "X" character. For example: X'53514C697465'
  22987. var P: PAnsiChar;
  22988. begin
  22989. result := '';
  22990. if RawBlobLength<>0 then begin
  22991. SetLength(result,RawBlobLength*2+3);
  22992. P := pointer(result);
  22993. P[0] := 'X';
  22994. P[1] := '''';
  22995. BinToHex(RawBlob,P+2,RawBlobLength);
  22996. P[RawBlobLength*2+2] := '''';
  22997. end;
  22998. end;
  22999. function isBlobHex(P: PUTF8Char): boolean;
  23000. // BLOB literals are string literals containing hexadecimal data and
  23001. // preceded by a single "x" or "X" character. For example: X'53514C697465'
  23002. var Len: integer;
  23003. begin
  23004. if P=nil then begin
  23005. result := false;
  23006. exit;
  23007. end;
  23008. while P^ in [#1..' '] do inc(P);
  23009. if (P[0] in ['x','X']) and (P[1]='''') then begin
  23010. Len := (StrLen(P)-3) shr 1;
  23011. result := (P[Len-1]='''') and SynCommons.HexToBin(@P[2],nil,Len);
  23012. exit;
  23013. end else begin
  23014. result := false;
  23015. exit;
  23016. end;
  23017. end;
  23018. function TSQLTable.GetBlob(Row, Field: integer): TSQLRawBlob;
  23019. begin
  23020. result := BlobToTSQLRawBlob(Get(Row,Field));
  23021. end;
  23022. function TSQLTable.GetBytes(Row,Field: integer): TBytes;
  23023. begin
  23024. result := BlobToBytes(Get(Row,Field));
  23025. end;
  23026. function TSQLTable.GetStream(Row,Field: integer): TStream;
  23027. begin
  23028. result := BlobToStream(Get(Row,Field));
  23029. end;
  23030. {$ifdef PUREPASCAL}
  23031. function TSQLTable.GetDateTime(Row, Field: integer): TDateTime;
  23032. begin
  23033. result := Iso8601ToDateTimePUTF8Char(Get(Row,Field),0)
  23034. end;
  23035. {$else}
  23036. function TSQLTable.GetDateTime(Row, Field: integer): TDateTime;
  23037. asm
  23038. call TSQLTable.Get
  23039. xor edx,edx // L=0 -> will call strlen()
  23040. jmp Iso8601ToDateTimePUTF8Char
  23041. end;
  23042. {$endif}
  23043. procedure TSQLTable.GetRowValues(Field: integer; out Values: TRawUTF8DynArray);
  23044. var i: integer;
  23045. U: PPUTF8Char;
  23046. begin
  23047. Finalize(Values);
  23048. if (self=nil) or (cardinal(Field)>cardinal(FieldCount)) then
  23049. exit;
  23050. SetLength(Values,fRowCount);
  23051. U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names)
  23052. for i := 0 to fRowCount-1 do begin
  23053. SetString(Values[i],PAnsiChar(U^),StrLen(U^));
  23054. inc(U,FieldCount); // go to next row
  23055. end;
  23056. end;
  23057. procedure TSQLTable.GetRowValues(Field: integer; out Values: TInt64DynArray);
  23058. var i: integer;
  23059. U: PPUTF8Char;
  23060. begin
  23061. Finalize(Values);
  23062. if (self=nil) or (cardinal(Field)>cardinal(FieldCount)) then
  23063. exit;
  23064. SetLength(Values,fRowCount);
  23065. U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names)
  23066. for i := 0 to fRowCount-1 do begin
  23067. SetInt64(U^,Values[i]);
  23068. inc(U,FieldCount); // go to next row
  23069. end;
  23070. end;
  23071. function TSQLTable.GetRowValues(Field: integer; Sep: AnsiChar): RawUTF8;
  23072. var i, L: integer;
  23073. U: PPUTF8Char;
  23074. P: PUTF8Char;
  23075. begin
  23076. result := '';
  23077. if (self=nil) or (cardinal(Field)>cardinal(FieldCount)) or (fRowCount=0) then
  23078. exit;
  23079. L := 0;
  23080. U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names)
  23081. for i := 1 to fRowCount do begin
  23082. inc(L,StrLen(U^)+1);
  23083. inc(U,FieldCount); // go to next row
  23084. end;
  23085. if L=0 then
  23086. exit;
  23087. SetLength(result,L-1); // L-1 = don't add a last ','
  23088. P := pointer(result);
  23089. U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names)
  23090. for i := 1 to fRowCount do begin
  23091. L := StrLen(U^);
  23092. MoveFast(U^^,P^,L);
  23093. if i=fRowCount then // don't add a last ','
  23094. break;
  23095. P[L] := Sep;
  23096. inc(P,L+1);
  23097. inc(U,FieldCount); // go to next row
  23098. end;
  23099. end;
  23100. procedure TSQLTable.GetJSONValues(JSON: TStream; Expand: boolean;
  23101. RowFirst: integer=0; RowLast: integer=0);
  23102. var W: TJSONWriter;
  23103. F,R: integer;
  23104. U: PPUTF8Char;
  23105. directWrites: set of 0..255;
  23106. begin
  23107. W := TJSONWriter.Create(JSON,Expand,false);
  23108. try
  23109. if (self=nil) or (FieldCount<=0) or (fRowCount<=0) then begin
  23110. W.CancelAllVoid;
  23111. exit;
  23112. end;
  23113. // check range
  23114. if RowLast=0 then
  23115. RowLast := fRowCount else
  23116. if RowLast>fRowCount then
  23117. RowLast := fRowCount;
  23118. if RowFirst<=0 then
  23119. RowFirst := 1; // start reading after first Row (Row 0 = Field Names)
  23120. // get col names and types
  23121. if QueryTables<>nil then
  23122. InitFieldTypes;
  23123. SetLength(W.ColNames,FieldCount);
  23124. FillCharFast(directWrites,(FieldCount shr 3)+1,0);
  23125. for F := 0 to FieldCount-1 do begin
  23126. W.ColNames[F] := fResults[F]; // first Row is field Names
  23127. if (QueryTables<>nil) and not Assigned(OnExportValue) then
  23128. with fFieldType[F] do
  23129. if SQLFieldTypeToDBField(ContentType,ContentTypeInfo) in
  23130. [ftInt64,ftDouble,ftCurrency] then
  23131. include(directWrites,F);
  23132. end;
  23133. W.AddColumns(RowLast-RowFirst+1); // write or init field names (see JSON Expand)
  23134. if Expand then
  23135. W.Add('[');
  23136. // write rows data
  23137. U := @fResults[FieldCount*RowFirst];
  23138. for R := RowFirst to RowLast do begin
  23139. if Expand then
  23140. W.Add('{');
  23141. for F := 0 to FieldCount-1 do begin
  23142. if Expand then
  23143. W.AddString(W.ColNames[F]); // '"'+ColNames[]+'":'
  23144. if Assigned(OnExportValue) then
  23145. W.AddString(OnExportValue(self,R,F)) else
  23146. if U^=nil then
  23147. W.AddShort('null') else
  23148. // IsStringJSON() is fast and safe: no need to guess exact value type
  23149. if (F in directWrites) or not IsStringJSON(U^) then
  23150. W.AddNoJSONEscape(U^,StrLen(U^)) else begin
  23151. W.Add('"');
  23152. W.AddJSONEscape(U^,StrLen(U^));
  23153. W.Add('"');
  23154. end;
  23155. W.Add(',');
  23156. inc(U); // points to next value
  23157. end;
  23158. W.CancelLastComma; // cancel last ','
  23159. if Expand then begin
  23160. W.Add('}',',');
  23161. if R<>RowLast then
  23162. W.AddCR; // make expanded json more human readable
  23163. end else
  23164. W.Add(',');
  23165. end;
  23166. W.EndJSONObject(1,0); // "RowCount": set by W.AddColumns(RowLast-RowFirst+1)
  23167. finally
  23168. W.Free;
  23169. end;
  23170. end;
  23171. procedure TSQLTable.GetJSONValues(W: TTextWriter; Expand: boolean;
  23172. RowFirst: integer=0; RowLast: integer=0);
  23173. begin
  23174. if (self=nil) or (FieldCount<=0) or (fRowCount<=0) then
  23175. W.Add('[',']') else begin
  23176. W.FlushToStream;
  23177. GetJSONValues(W.Stream,Expand,RowFirst,RowLast);
  23178. end;
  23179. end;
  23180. procedure TSQLTable.GetCSVValues(Dest: TStream; Tab: boolean; CommaSep: AnsiChar=',';
  23181. AddBOM: boolean=false; RowFirst: integer=0; RowLast: integer=0);
  23182. var U: PPUTF8Char;
  23183. F,R,FMax: integer;
  23184. W: TTextWriter;
  23185. begin
  23186. if (self=nil) or (FieldCount<=0) or (fRowCount<=0) then
  23187. exit;
  23188. if (RowLast=0) or (RowLast>fRowCount) then
  23189. RowLast := fRowCount;
  23190. if RowFirst<0 then
  23191. RowFirst := 0;
  23192. W := TTextWriter.Create(Dest,16384);
  23193. try
  23194. if AddBOM then
  23195. W.AddShort(#$ef#$bb#$bf); // add UTF-8 Byte Order Mark
  23196. if Tab then
  23197. CommaSep := #9;
  23198. FMax := FieldCount-1;
  23199. U := @fResults[RowFirst*FieldCount];
  23200. for R := RowFirst to RowLast do
  23201. for F := 0 to FMax do begin
  23202. if Assigned(OnExportValue) then
  23203. W.AddString(OnExportValue(self,R,F)) else
  23204. if Tab or (not IsStringJSON(U^)) then
  23205. W.AddNoJSONEscape(U^,StrLen(U^)) else begin
  23206. W.Add('"');
  23207. W.AddNoJSONEscape(U^,StrLen(U^));
  23208. W.Add('"');
  23209. end;
  23210. if F=FMax then
  23211. W.AddCR else
  23212. W.Add(CommaSep);
  23213. inc(U); // points to next value
  23214. end;
  23215. W.FlushFinal;
  23216. finally
  23217. W.Free;
  23218. end;
  23219. end;
  23220. function TSQLTable.GetCSVValues(Tab: boolean; CommaSep: AnsiChar=',';
  23221. AddBOM: boolean=false; RowFirst: integer=0; RowLast: integer=0): RawUTF8;
  23222. var MS: TRawByteStringStream;
  23223. begin
  23224. MS := TRawByteStringStream.Create;
  23225. try
  23226. GetCSVValues(MS,Tab,CommaSep,AddBOM,RowFirst,RowLast);
  23227. result := MS.DataString;
  23228. finally
  23229. MS.Free;
  23230. end;
  23231. end;
  23232. procedure TSQLTable.GetMSRowSetValues(Dest: TStream; RowFirst,RowLast: integer);
  23233. const FIELDTYPE_TOXML: array[TSQLDBFieldType] of RawUTF8 = (
  23234. // ftUnknown, ftNull, ftInt64, ftDouble, ftCurrency,
  23235. '','',' dt:type="i8"',' dt:type="float"',' dt:type="number" rs:dbtype="currency"',
  23236. // ftDate, ftUTF8, ftBlob
  23237. ' dt:type="dateTime"',' dt:type="string"',' dt:type="bin.hex"');
  23238. var W: TJSONWriter;
  23239. f,r: integer;
  23240. U: PPUTF8Char;
  23241. fieldType: TSQLDBFieldTypeDynArray;
  23242. begin
  23243. W := TJSONWriter.Create(Dest,16384);
  23244. try
  23245. W.AddShort('<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" '+
  23246. 'xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" '+
  23247. 'xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">');
  23248. if (self<>nil) and (FieldCount>0) or (fRowCount>0) then begin
  23249. // retrieve normalized field names and types
  23250. if length(fFieldNames)<>fFieldCount then
  23251. InitFieldNames;
  23252. if not Assigned(fFieldType) then
  23253. InitFieldTypes;
  23254. SetLength(fieldType,FieldCount);
  23255. for f := 0 to FieldCount-1 do
  23256. with fFieldType[F] do
  23257. fieldType[f] := SQLFieldTypeToDBField(ContentType,ContentTypeInfo);
  23258. // check range
  23259. if RowLast=0 then
  23260. RowLast := fRowCount else
  23261. if RowLast>fRowCount then
  23262. RowLast := fRowCount;
  23263. if RowFirst<=0 then
  23264. RowFirst := 1; // start reading after first Row (Row 0 = Field Names)
  23265. // write schema from col names and types
  23266. W.AddShort('<s:Schema id="RowsetSchema"><s:ElementType name="row" content="eltOnly">');
  23267. for f := 0 to FieldCount-1 do begin
  23268. W.AddShort('<s:AttributeType name="f');
  23269. W.Add(f);
  23270. W.AddShort('" rs:name="');
  23271. W.AddString(fFieldNames[f]);
  23272. W.Add('"');
  23273. W.AddString(FIELDTYPE_TOXML[fieldType[f]]);
  23274. W.Add('/','>');
  23275. end;
  23276. W.AddShort('</s:ElementType></s:Schema>');
  23277. // write rows data
  23278. U := @fResults[FieldCount*RowFirst];
  23279. W.AddShort('<rs:data>');
  23280. for r := RowFirst to RowLast do begin
  23281. W.AddShort('<z:row ');
  23282. for f := 0 to FieldCount-1 do begin
  23283. if U^<>nil then begin
  23284. W.Add('f');
  23285. W.Add(f);
  23286. W.Add('=','"');
  23287. case fieldType[f] of
  23288. ftUnknown:
  23289. if IsStringJSON(U^) then // no need to guess exact value type here
  23290. W.AddXmlEscape(U^) else
  23291. W.AddNoJSONEscape(U^,StrLen(U^));
  23292. ftInt64, ftDouble, ftCurrency:
  23293. W.AddNoJSONEscape(U^,StrLen(U^));
  23294. ftDate, ftUTF8, ftBlob:
  23295. W.AddXmlEscape(U^);
  23296. end;
  23297. W.Add('"',' ');
  23298. end;
  23299. inc(U); // points to next value
  23300. end;
  23301. W.Add('/','>');
  23302. end;
  23303. W.AddShort('</rs:data>');
  23304. end;
  23305. W.AddShort('</xml>');
  23306. W.FlushFinal;
  23307. finally
  23308. W.Free;
  23309. end;
  23310. end;
  23311. function TSQLTable.GetMSRowSetValues: RawUTF8;
  23312. var MS: TRawByteStringStream;
  23313. begin
  23314. MS := TRawByteStringStream.Create;
  23315. try
  23316. GetMSRowSetValues(MS,1,RowCount);
  23317. result := MS.DataString;
  23318. finally
  23319. MS.Free;
  23320. end;
  23321. end;
  23322. function TSQLTable.GetODSDocument: RawByteString;
  23323. const
  23324. ODSmimetype: RawUTF8 = 'application/vnd.oasis.opendocument.spreadsheet';
  23325. ODSContentHeader: RawUTF8 = '<office:document-content office:version="1.2" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"'+
  23326. ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"'+
  23327. ' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" ><office:body><office:spreadsheet><table:table table:name="Sheet1" >'+
  23328. '<table:table-column table:number-columns-repeated="';
  23329. ODSContentFooter = '</table:table><table:named-expressions/></office:spreadsheet></office:body></office:document-content>';
  23330. ODSstyles: RawUTF8 = XMLUTF8_HEADER+'<office:document-styles xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" office:version="1.2"></office:document-styles>';
  23331. ODSmeta: RawUTF8 = XMLUTF8_HEADER+'<office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" office:version="1.2"></office:document-meta>';
  23332. ODSsettings: RawUTF8 = XMLUTF8_HEADER+'<office:document-settings xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" office:version="1.2"></office:document-settings>';
  23333. ODSmanifest: RawUTF8 = XMLUTF8_HEADER+'<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"'+
  23334. ' manifest:version="1.2"><manifest:file-entry manifest:full-path="/" manifest:version="1.2" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>'+
  23335. '<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/><manifest:file-entry manifest:full-path="settings.xml" manifest:media-type="text/xml"/>'+
  23336. '<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/><manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/></manifest:manifest>';
  23337. var Zip: TZipWriteToStream;
  23338. Dest: TRawByteStringStream;
  23339. content: RawUTF8;
  23340. W: TTextWriter;
  23341. U: PPUTF8Char;
  23342. R,F: integer;
  23343. begin
  23344. Dest := TRawByteStringStream.Create;
  23345. try
  23346. Zip := TZipWriteToStream.Create(Dest);
  23347. try
  23348. Zip.AddStored('mimetype',pointer(ODSmimetype),length(ODSmimetype));
  23349. Zip.AddDeflated('styles.xml',pointer(ODSstyles),length(ODSstyles));
  23350. Zip.AddDeflated('meta.xml',pointer(ODSmeta),length(ODSmeta));
  23351. Zip.AddDeflated('settings.xml',pointer(ODSsettings),length(ODSsettings));
  23352. Zip.AddDeflated('META-INF/manifest.xml',pointer(ODSmanifest),length(ODSmanifest));
  23353. W := TTextWriter.CreateOwnedStream(65536);
  23354. try
  23355. W.AddShort(XMLUTF8_HEADER);
  23356. W.AddString(ODSContentHeader);
  23357. W.Add(FieldCount);
  23358. W.AddShort('" />');
  23359. U := pointer(fResults);
  23360. for R := 0 to fRowCount do begin
  23361. W.AddShort('<table:table-row>');
  23362. for F := 1 to FieldCount do begin
  23363. W.AddShort('<table:table-cell office:value-type="string"><text:p>');
  23364. W.AddXmlEscape(U^);
  23365. W.AddShort('</text:p></table:table-cell>');
  23366. inc(U); // points to next value
  23367. end;
  23368. W.AddShort('</table:table-row>');
  23369. end;
  23370. W.AddShort(ODSContentFooter);
  23371. W.SetText(content);
  23372. finally
  23373. W.Free;
  23374. end;
  23375. Zip.AddDeflated('content.xml',pointer(content),length(content));
  23376. finally
  23377. Zip.Free;
  23378. end;
  23379. result := Dest.DataString;
  23380. finally
  23381. Dest.Free;
  23382. end;
  23383. end;
  23384. function TSQLTable.GetJSONValues(Expand: boolean): RawUTF8;
  23385. var MS: TRawByteStringStream;
  23386. begin
  23387. MS := TRawByteStringStream.Create;
  23388. try
  23389. GetJSONValues(MS,Expand); // create JSON data in MS
  23390. result := MS.DataString;
  23391. finally
  23392. MS.Free;
  23393. end;
  23394. end;
  23395. procedure TSQLTable.GetHtmlTable(Dest: TTextWriter);
  23396. var R,F: integer;
  23397. U: PPUTF8Char;
  23398. begin
  23399. Dest.AddShort('<table>'#10);
  23400. U := pointer(fResults);
  23401. for R := 0 to fRowCount do begin
  23402. Dest.AddShort('<tr>');
  23403. for F := 0 to FieldCount-1 do begin
  23404. if R=0 then
  23405. Dest.AddShort('<th>') else
  23406. Dest.AddShort('<td>');
  23407. if Assigned(OnExportValue) and (R>0) then
  23408. Dest.AddHtmlEscapeUTF8(OnExportValue(self,R,F),hfOutsideAttributes) else
  23409. Dest.AddHtmlEscape(U^,hfOutsideAttributes);
  23410. if R=0 then
  23411. Dest.AddShort('</th>') else
  23412. Dest.AddShort('</td>');
  23413. inc(U); // points to next value
  23414. end;
  23415. Dest.AddShort('</tr>'#10);
  23416. end;
  23417. Dest.AddShort('</table>');
  23418. end;
  23419. function TSQLTable.GetHtmlTable(const Header: RawUTF8): RawUTF8;
  23420. var W: TTextWriter;
  23421. begin
  23422. W := TTextWriter.CreateOwnedStream(16384);
  23423. try
  23424. W.AddShort('<html>');
  23425. W.AddString(Header);
  23426. W.AddShort('<body>'#10);
  23427. GetHtmlTable(W);
  23428. W.AddShort(#10'</body></html>');
  23429. W.SetText(result);
  23430. finally
  23431. W.Free;
  23432. end;
  23433. end;
  23434. function TSQLTable.GetW(Row, Field: integer): RawUnicode;
  23435. begin
  23436. result := UTF8DecodeToRawUnicode(Get(Row,Field),0);
  23437. end;
  23438. function TSQLTable.GetWP(Row, Field: integer; Dest: PWideChar; MaxDestChars: cardinal): integer;
  23439. var P: PUTF8Char;
  23440. begin
  23441. P := Get(Row,Field);
  23442. result := UTF8ToWideChar(Dest,P,MaxDestChars,0) shr 1; // bytes div 2
  23443. end;
  23444. function TSQLTable.LengthW(Row, Field: integer): integer;
  23445. begin // nil -> fast calculate unicode length, without any memory allocation
  23446. result := Utf8ToUnicodeLength(Get(Row,Field));
  23447. end;
  23448. function UTF8CompareCurr64(P1,P2: PUTF8Char): PtrInt;
  23449. var V1,V2: Int64;
  23450. begin // faster than UTF8CompareDouble() for pure decimal (no exponent) values
  23451. V1 := StrToCurr64(P1);
  23452. V2 := StrToCurr64(P2);
  23453. if V1<V2 then
  23454. result := -1 else
  23455. if V1=V2 then
  23456. result := 0 else
  23457. result := +1;
  23458. end;
  23459. function UTF8CompareBoolean(P1,P2: PUTF8Char): PtrInt;
  23460. label Z,P,N;
  23461. begin // assume 0 is FALSE, anything else is true
  23462. if P1=P2 then goto Z else
  23463. if P1=nil then goto P else
  23464. if P2=nil then goto N else
  23465. if (P1^=#0) or (PWord(P1)^=ord('0')) then
  23466. if (P2^=#0) or (PWord(P2)^=ord('0')) then begin
  23467. Z: result := 0; // P1=false P2=false
  23468. exit;
  23469. end else begin
  23470. N: result := -1; // P1=false P2=true
  23471. exit;
  23472. end else
  23473. if (P2^<>#0) and (PWord(P2)^<>ord('0')) then
  23474. goto Z // P1=true P2=true
  23475. else begin
  23476. P: result := 1; // P1=true P2=false
  23477. exit;
  23478. end;
  23479. end;
  23480. function UTF8CompareInt32(P1,P2: PUTF8Char): PtrInt;
  23481. var V1,V2: PtrInt;
  23482. begin
  23483. if P1=P2 then begin
  23484. result := 0;
  23485. exit;
  23486. end;
  23487. V1 := GetInteger(P1);
  23488. V2 := GetInteger(P2);
  23489. if V1<V2 then
  23490. result := -1 else
  23491. if V1=V2 then
  23492. result := 0 else
  23493. result := +1;
  23494. end;
  23495. function UTF8CompareUInt32(P1,P2: PUTF8Char): PtrInt;
  23496. var V1,V2: PtrUInt;
  23497. begin
  23498. if P1=P2 then begin
  23499. result := 0;
  23500. exit;
  23501. end;
  23502. V1 := GetCardinal(P1);
  23503. V2 := GetCardinal(P2);
  23504. if V1<V2 then
  23505. result := -1 else
  23506. if V1=V2 then
  23507. result := 0 else
  23508. result := +1;
  23509. end;
  23510. function UTF8CompareRecord(P1,P2: PUTF8Char): PtrInt;
  23511. var V1,V2: Int64;
  23512. T1,T2: cardinal;
  23513. label er;
  23514. begin
  23515. if P1=P2 then begin
  23516. result := 0;
  23517. exit;
  23518. end;
  23519. SetInt64(P1,V1);
  23520. SetInt64(P2,V2);
  23521. if V1=V2 then
  23522. result := 0 else begin
  23523. // special RecordRef / TRecordReference INTEGER sort
  23524. T1 := V1 and 63; // first sort by Table order
  23525. T2 := V2 and 63;
  23526. if T1<T2 then
  23527. result := -1 else
  23528. if T1>T2 then
  23529. result := +1 else
  23530. // we have T1=T2 -> same Table -> sort by ID
  23531. if V1<V2 then
  23532. result := -1 else
  23533. if V1=V2 then
  23534. result := 0 else
  23535. result := +1;
  23536. end;
  23537. end;
  23538. function UTF8CompareInt64(P1,P2: PUTF8Char): PtrInt;
  23539. var V1,V2: Int64;
  23540. begin
  23541. if P1=P2 then begin
  23542. result := 0;
  23543. exit;
  23544. end;
  23545. SetInt64(P1,V1);
  23546. SetInt64(P2,V2);
  23547. if V1<V2 then
  23548. result := -1 else
  23549. if V1=V2 then
  23550. result := 0 else
  23551. result := +1;
  23552. end;
  23553. function UTF8CompareDouble(P1,P2: PUTF8Char): PtrInt;
  23554. var V1,V2: TSynExtended;
  23555. Err: integer;
  23556. label er;
  23557. begin
  23558. if P1=P2 then begin
  23559. result := 0;
  23560. exit;
  23561. end;
  23562. v1 := GetExtended(P1,Err);
  23563. if Err<>0 then begin
  23564. er: result := UTF8IComp(P1,P2);
  23565. exit;
  23566. end;
  23567. V2 := GetExtended(P2,Err);
  23568. if Err<>0 then goto er;
  23569. if V1<V2 then // we don't care about exact = for a sort: Epsilon check is slow
  23570. result := -1 else
  23571. result := +1;
  23572. end;
  23573. function UTF8CompareISO8601(P1,P2: PUTF8Char): PtrInt;
  23574. var V1,V2: Int64; // faster than Iso8601ToDateTimePUTF8Char: uses integer math
  23575. begin
  23576. if P1=P2 then begin
  23577. result := 0;
  23578. exit;
  23579. end;
  23580. V1 := Iso8601ToTimeLogPUTF8Char(P1,0);
  23581. V2 := Iso8601ToTimeLogPUTF8Char(P2,0);
  23582. if (V1=0) or (V2=0) then // any invalid date -> compare as strings
  23583. result := UTF8IComp(P1,P2) else
  23584. if V1<V2 then
  23585. result := -1 else
  23586. if V1=V2 then
  23587. result := 0 else
  23588. result := +1;
  23589. end;
  23590. var
  23591. /// simple wrapper to UTF-8 compare function for the SQLite3 field datatypes
  23592. // - used internaly for field sorting (see TSQLTable.SortFields() method)
  23593. // and for default User Interface Query (see TSQLRest.QueryIsTrue() method)
  23594. SQLFieldTypeComp: array[TSQLFieldType] of TUTF8Compare =
  23595. (nil, // unknown
  23596. nil, // AnsiText will be set to AnsiIComp in initialization block below
  23597. {$ifdef USENORMTOUPPER}
  23598. UTF8IComp, // UTF8Text, 8 bits case insensitive compared
  23599. {$else}
  23600. nil, // UTF8Text will be set to AnsiIComp in initialization block below
  23601. {$endif}
  23602. UTF8CompareUInt32, // Enumerate
  23603. UTF8CompareUInt32, // Set
  23604. UTF8CompareInt64, // Integer
  23605. UTF8CompareInt64, // ID
  23606. UTF8CompareRecord, // Record
  23607. UTF8CompareBoolean, // Boolean
  23608. UTF8CompareDouble, // Float
  23609. UTF8CompareISO8601, // TDateTime
  23610. UTF8CompareInt64, // TTimeLog
  23611. UTF8CompareCurr64, // Currency
  23612. nil, // Object (TEXT serialization)
  23613. {$ifndef NOVARIANTS}
  23614. nil, // Variant (TEXT serialization)
  23615. nil, // TNullable*
  23616. {$endif}
  23617. nil, // Blob
  23618. nil, // BlobDynArray
  23619. nil, // BlobCustom
  23620. nil, // UTF8Custom
  23621. nil,
  23622. UTF8CompareInt64, // TModTime
  23623. UTF8CompareInt64, // TCreateTime
  23624. UTF8CompareInt64, // TID
  23625. UTF8CompareInt64, // TRecordVersion
  23626. UTF8CompareInt64); // TSessionUserID
  23627. type
  23628. /// a static object is used for smaller recursive stack size and faster code
  23629. // - these special sort implementation do the comparaison first by the
  23630. // designed field, and, if the field value is identical, the ID value is
  23631. // used (it will therefore sort by time all identical values)
  23632. // - code generated is very optimized: stack and memory usage, CPU registers
  23633. // prefered, multiplication avoided to calculate memory position from index,
  23634. // hand tuned assembler...
  23635. TUTF8QuickSort = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  23636. public
  23637. // sort parameters
  23638. fComp: TUTF8Compare;
  23639. Results: PPUtf8CharArray;
  23640. IDColumn: PPUtf8CharArray;
  23641. Params: TSQLTableSortParams;
  23642. CurrentRow: integer;
  23643. // used to avoid multiplications to calculate data memory position from index
  23644. // - CPU64 ready
  23645. FieldCountNextPtr, FieldIndexNextPtr: PtrInt;
  23646. // temp vars (avoid stack usage):
  23647. PID: Int64;
  23648. PP, CI, CJ: PPUTF8Char;
  23649. I, J: PtrInt;
  23650. {$ifdef PUREPASCAL}
  23651. Tmp: PUTF8Char;
  23652. {$endif}
  23653. /// recursively perform the sort
  23654. procedure QuickSort(L, R: Integer);
  23655. /// compare value at index I with pivot value
  23656. // - sort by ID if values are identical
  23657. function CompI: integer; {$ifdef HASINLINE}inline;{$endif}
  23658. /// compare value at index J with pivot value
  23659. // - sort by ID if values are identical
  23660. function CompJ: integer; {$ifdef HASINLINE}inline;{$endif}
  23661. /// set the pivot value
  23662. procedure SetPP(aPP: PPUTF8Char; aP: PtrInt);
  23663. end;
  23664. procedure TUTF8QuickSort.SetPP(aPP: PPUTF8Char; aP: PtrInt);
  23665. begin
  23666. PP := aPP;
  23667. // PID must be updated every time PP is modified
  23668. if Assigned(IDColumn) then
  23669. SetInt64(IDColumn[aP],PID) else
  23670. SetInt64(PPUTF8Char(PtrInt(aPP)-FieldIndexNextPtr)^,PID);
  23671. end;
  23672. function TUTF8QuickSort.CompI: integer;
  23673. begin
  23674. result := fComp(CI^,PP^);
  23675. if result=0 then
  23676. // same value -> sort by ID
  23677. if Assigned(IDColumn) then
  23678. result := GetInt64(IDColumn[I])-PID else
  23679. result := GetInt64(PPUTF8Char(PtrInt(CI)-FieldIndexNextPtr)^)-PID;
  23680. end;
  23681. function TUTF8QuickSort.CompJ: integer;
  23682. begin
  23683. result := fComp(CJ^,PP^);
  23684. if result=0 then
  23685. // same value -> sort by ID
  23686. if Assigned(IDColumn) then
  23687. result := GetInt64(IDColumn[J])-PID else
  23688. result := GetInt64(PPUTF8Char(PtrInt(CJ)-FieldIndexNextPtr)^)-PID;
  23689. end;
  23690. procedure ExchgPtrUInt(P1,P2: PtrUInt; FieldCount: integer);
  23691. {$ifdef PUREPASCAL} // CPU64 will call this version e.g.
  23692. var B: PtrUInt;
  23693. i: PtrUInt;
  23694. begin
  23695. for i := 1 to FieldCount do begin
  23696. B := PPtrUInt(P1)^;
  23697. PPtrUInt(P1)^ := PPtrUInt(P2)^;
  23698. PPtrUInt(P2)^ := B;
  23699. inc(PPtrUInt(P1));
  23700. inc(PPtrUInt(P2));
  23701. end;
  23702. end;
  23703. {$else}
  23704. asm // eax=P1 edx=P2 ecx=FieldCount
  23705. push esi
  23706. push edi
  23707. @1: dec ecx
  23708. mov esi,[eax]
  23709. mov edi,[edx]
  23710. mov [edx],esi
  23711. mov [eax],edi
  23712. lea eax,[eax+4]
  23713. lea edx,[edx+4]
  23714. jnz @1
  23715. pop edi
  23716. pop esi
  23717. end;
  23718. {$endif}
  23719. procedure TUTF8QuickSort.QuickSort(L, R: Integer);
  23720. {$ifndef PUREPASCAL}
  23721. procedure Exchg32(P: pointer; I,J: integer);
  23722. asm // eax=P edx=I ecx=J
  23723. push ebx
  23724. lea edx,[eax+edx*4]
  23725. lea ecx,[eax+ecx*4]
  23726. mov eax,[edx]
  23727. mov ebx,[ecx]
  23728. mov [ecx],eax
  23729. mov [edx],ebx
  23730. pop ebx
  23731. end;
  23732. {$endif}
  23733. // code below is very fast and optimized
  23734. var P: PtrInt;
  23735. begin
  23736. if @fComp<>nil then
  23737. repeat
  23738. I := L;
  23739. CI := @Results[I*Params.FieldCount+Params.FieldIndex];
  23740. J := R;
  23741. CJ := @Results[J*Params.FieldCount+Params.FieldIndex];
  23742. P := ((I+J) shr 1);
  23743. SetPP(@Results[P*Params.FieldCount+Params.FieldIndex],P);
  23744. repeat
  23745. // this loop has no multiplication -> most of the time is spent in comp()
  23746. if Params.Asc then begin // ascending order comparaison
  23747. while compI<0 do begin
  23748. inc(I);
  23749. inc(PByte(CI),FieldCountNextPtr); // next row
  23750. end;
  23751. while compJ>0 do begin
  23752. dec(J);
  23753. dec(PByte(CJ),FieldCountNextPtr); // previous row
  23754. end;
  23755. end else begin // descending order comparaison
  23756. while compI>0 do begin
  23757. inc(I);
  23758. inc(PByte(CI),FieldCountNextPtr); // next row
  23759. end;
  23760. while compJ<0 do begin
  23761. dec(J);
  23762. dec(PByte(CJ),FieldCountNextPtr); // previous row
  23763. end;
  23764. end;
  23765. if I<=J then begin
  23766. if I<>J then begin // swap elements
  23767. if CurrentRow=J then // update current row number
  23768. CurrentRow := I else
  23769. if CurrentRow=I then
  23770. CurrentRow := J;
  23771. // full row exchange
  23772. ExchgPtrUInt(PtrInt(CI)-FieldIndexNextPtr,PtrInt(CJ)-FieldIndexNextPtr,
  23773. Params.FieldCount); // exchange PUTF8Char for whole I,J rows
  23774. if Assigned(IDColumn) then begin // update hidden ID column also
  23775. {$ifdef PUREPASCAL}
  23776. Tmp := IDColumn[I];
  23777. IDColumn[I] := IDColumn[J];
  23778. IDColumn[J] := Tmp;
  23779. {$else}
  23780. Exchg32(IDColumn,I,J);
  23781. {$endif}
  23782. end;
  23783. end;
  23784. if PP=CI then
  23785. SetPP(CJ,J) else
  23786. if PP=CJ then
  23787. SetPP(CI,I);
  23788. inc(I);
  23789. dec(J);
  23790. inc(PByte(CI),FieldCountNextPtr);
  23791. dec(PByte(CJ),FieldCountNextPtr);
  23792. end else
  23793. break;
  23794. until I>J;
  23795. P := I; // save I which will be overwritten by QuickSort() below
  23796. if L<J then
  23797. QuickSort(L, J);
  23798. I := P;
  23799. L := P;
  23800. until I>=R;
  23801. end;
  23802. procedure TSQLTable.SortFields(const FieldName: RawUTF8; Asc: boolean=true;
  23803. PCurrentRow: PInteger=nil; FieldType: TSQLFieldType=sftUnknown;
  23804. CustomCompare: TUTF8Compare=nil);
  23805. begin
  23806. SortFields(FieldIndex(FieldName),Asc,PCurrentRow,FieldType,CustomCompare);
  23807. end;
  23808. procedure TSQLTable.SortFields(Field: integer; Asc: boolean=true;
  23809. PCurrentRow: PInteger=nil; FieldType: TSQLFieldType=sftUnknown;
  23810. CustomCompare: TUTF8Compare=nil);
  23811. var Sort: TUTF8QuickSort; // fast static object for sorting
  23812. begin
  23813. if (FieldCount=0) or (Cardinal(Field)>=cardinal(FieldCount)) then
  23814. exit;
  23815. if FieldType=sftUnknown then // guess the field type from first row
  23816. FieldType := self.FieldType(Field,nil);
  23817. if Assigned(CustomCompare) then
  23818. Sort.fComp := CustomCompare else begin
  23819. Sort.fComp := SQLFieldTypeComp[FieldType];
  23820. if @Sort.fComp=nil then
  23821. exit;
  23822. end;
  23823. // store sorting parameters for resort in TSQLTableJSON.FillFrom()
  23824. fSortParams.FieldType := FieldType;
  23825. fSortParams.FieldCount := FieldCount;
  23826. fSortParams.FieldIndex := Field;
  23827. fSortParams.Asc := Asc;
  23828. // this sort routine is very fast, thanks to the dedicated static object
  23829. Sort.Params := fSortParams;
  23830. Sort.Results := fResults;
  23831. Sort.IDColumn := @fIDColumn[0];
  23832. Sort.FieldCountNextPtr := FieldCount*sizeof(PtrInt);
  23833. Sort.FieldIndexNextPtr := Field*sizeof(PtrInt);
  23834. if PCurrentRow=nil then
  23835. Sort.CurrentRow := -1 else
  23836. Sort.CurrentRow := PCurrentRow^;
  23837. if fRowCount>1 then
  23838. Sort.QuickSort(1,fRowCount); // ignore first row = field names -> (1,RowCount)
  23839. if PCurrentRow<>nil then
  23840. PCurrentRow^ := Sort.CurrentRow;
  23841. end;
  23842. type
  23843. TUTF8QuickSortMulti = {$ifndef ISDELPHI2010}object{$else}record{$endif}
  23844. public
  23845. Results: PPUtf8CharArray;
  23846. IDColumn: PPUtf8CharArray;
  23847. FieldCount: integer;
  23848. IndexMax: integer;
  23849. Index: array of record
  23850. ndx: integer;
  23851. Comp: TUTF8Compare;
  23852. Desc: boolean;
  23853. end;
  23854. // used for row content comparison
  23855. function Compare(A,B: integer): integer;
  23856. /// recursively perform the sort
  23857. procedure QuickSort(L, R: Integer);
  23858. end;
  23859. function TUTF8QuickSortMulti.Compare(A,B: integer): integer;
  23860. var i: integer;
  23861. begin
  23862. result := 0;
  23863. for i := 0 to IndexMax do
  23864. with Index[i] do begin
  23865. if ndx>=0 then
  23866. result := Comp(Results[A*FieldCount+ndx],Results[B*FieldCount+ndx]) else
  23867. // Fields[].ndx=-1 for hidden ID column
  23868. result := GetInt64(IDColumn[A])-GetInt64(IDColumn[B]);
  23869. if result<>0 then begin
  23870. if Desc then
  23871. result := -result; // descending order -> inverse comparison
  23872. exit;
  23873. end;
  23874. end;
  23875. end;
  23876. procedure TUTF8QuickSortMulti.QuickSort(L, R: Integer);
  23877. var I,J,P: integer;
  23878. Tmp: PUTF8Char;
  23879. begin
  23880. if L<R then
  23881. repeat
  23882. I := L;
  23883. J := R;
  23884. P := (L+R) shr 1;
  23885. repeat
  23886. while Compare(I,P)<0 do inc(I);
  23887. while Compare(J,P)>0 do dec(J);
  23888. if I<=J then begin
  23889. if I<>J then begin // swap elements
  23890. ExchgPtrUInt(PtrUInt(@Results[I*FieldCount]),
  23891. PtrUInt(@Results[J*FieldCount]),FieldCount);
  23892. if Assigned(IDColumn) then begin // update hidden ID column also
  23893. Tmp := IDColumn[I];
  23894. IDColumn[I] := IDColumn[J];
  23895. IDColumn[J] := Tmp;
  23896. end;
  23897. end;
  23898. if P=I then
  23899. P := J else
  23900. if P=J then
  23901. P := I;
  23902. inc(I);
  23903. dec(J);
  23904. end;
  23905. until I>J;
  23906. if L<J then
  23907. QuickSort(L,J);
  23908. L := I;
  23909. until I >= R;
  23910. end;
  23911. procedure TSQLTable.SortFields(const Fields: array of integer;
  23912. const Asc: array of boolean);
  23913. var Sort: TUTF8QuickSortMulti;
  23914. i: integer;
  23915. begin
  23916. if (self=nil) or (fRowCount<=1) or (FieldCount<=0) or (length(Fields)=0) then
  23917. exit;
  23918. Sort.FieldCount := FieldCount;
  23919. Sort.IndexMax := high(Fields);
  23920. SetLength(Sort.Index,Sort.IndexMax+1);
  23921. for i := 0 to Sort.IndexMax do
  23922. with Sort.Index[i] do begin
  23923. ndx := Fields[i];
  23924. if ndx<0 then begin // Fields[]=-1 for ID column
  23925. if not Assigned(fIDColumn) then begin // leave ndx<0 for hidden ID
  23926. ndx := fFieldIndexID; // use the ID column
  23927. if ndx<0 then
  23928. exit; // no ID column available
  23929. Comp := @UTF8CompareInt64;
  23930. end;
  23931. continue;
  23932. end;
  23933. Comp := SortCompare(ndx);
  23934. if @Comp=nil then
  23935. exit; // impossible to sort this kind of field (or invalid field index)
  23936. end;
  23937. for i := 0 to high(Asc) do
  23938. if (i<=Sort.IndexMax) and not Asc[i] then
  23939. Sort.Index[i].Desc := true;
  23940. Sort.Results := fResults;
  23941. Sort.IDColumn := @fIDColumn[0];
  23942. Sort.QuickSort(1,fRowCount); // ignore first row = field names -> (1,RowCount)
  23943. end;
  23944. function TSQLTable.SortCompare(Field: integer): TUTF8Compare;
  23945. begin
  23946. result := SQLFieldTypeComp[FieldType(Field,nil)];
  23947. end;
  23948. procedure TSQLTable.Assign(source: TSQLTable);
  23949. begin
  23950. fResults := source.fResults;
  23951. fRowCount := source.fRowCount;
  23952. fFieldCount := source.fFieldCount;
  23953. end;
  23954. constructor TSQLTable.Create(const aSQL: RawUTF8);
  23955. begin
  23956. fQuerySQL := aSQL;
  23957. fFieldIndexID := -1;
  23958. fQueryTableIndexFromSQL := -2; // indicates not searched
  23959. end;
  23960. constructor TSQLTable.CreateFromTables(const Tables: array of TSQLRecordClass;
  23961. const aSQL: RawUTF8);
  23962. var n: integer;
  23963. begin
  23964. Create(aSQL);
  23965. n := length(Tables);
  23966. if n>0 then begin
  23967. SetLength(fQueryTables,n);
  23968. MoveFast(Tables[0],fQueryTables[0],n*sizeof(TClass));
  23969. end;
  23970. end;
  23971. constructor TSQLTable.CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType;
  23972. const aSQL: RawUTF8);
  23973. begin
  23974. Create(aSQL);
  23975. SetLength(fQueryColumnTypes,length(ColumnTypes));
  23976. MoveFast(ColumnTypes[0],fQueryColumnTypes[0],length(ColumnTypes)*sizeof(TSQLFieldType));
  23977. end;
  23978. destructor TSQLTable.Destroy;
  23979. begin
  23980. fOwnedRecords.Free;
  23981. inherited;
  23982. end;
  23983. function TSQLTable.QueryRecordType: TSQLRecordClass;
  23984. begin
  23985. if (self<>nil) and (pointer(fQueryTables)<>nil) then
  23986. result := fQueryTables[0] else
  23987. result := nil;
  23988. end;
  23989. function TSQLTable.NewRecord(RecordType: TSQLRecordClass=nil): TSQLRecord;
  23990. begin
  23991. result := nil;
  23992. if self=nil then
  23993. exit;
  23994. if RecordType=nil then begin
  23995. RecordType := QueryRecordType;
  23996. if RecordType=nil then
  23997. exit;
  23998. end;
  23999. result := RecordType.Create;
  24000. if fOwnedRecords=nil then
  24001. fOwnedRecords := TObjectList.Create;
  24002. fOwnedRecords.Add(result);
  24003. end;
  24004. {$ifdef ISDELPHI2010} // Delphi 2009 generics are buggy
  24005. function TSQLTable.ToObjectList<T>: TObjectList<T>;
  24006. var R,Item: TSQLRecord;
  24007. Row: PPUtf8Char;
  24008. i: integer;
  24009. begin
  24010. result := TObjectList<T>.Create; // TObjectList<T> will free each T instance
  24011. if (self=nil) or (fRowCount=0) then
  24012. exit;
  24013. R := TSQLRecordClass(T).Create;
  24014. try
  24015. R.FillPrepare(self);
  24016. Row := @fResults[FieldCount]; // Row^ points to first row of data
  24017. {$ifdef ISDELPHIXE3}
  24018. result.Count := fRowCount; // faster than manual Add()
  24019. for i := 0 to fRowCount-1 do begin
  24020. Item := TSQLRecordClass(T).Create;
  24021. PPointerArray(result.List)[i] := Item;
  24022. {$else}
  24023. for i := 0 to fRowCount-1 do begin
  24024. Item := TSQLRecordClass(T).Create;
  24025. Result.Add(Item);
  24026. {$endif}
  24027. R.fFill.Fill(pointer(Row),Item);
  24028. Inc(Row,FieldCount); // next data row
  24029. end;
  24030. finally
  24031. R.Free;
  24032. end;
  24033. end;
  24034. {$endif}
  24035. procedure TSQLTable.ToObjectList(DestList: TObjectList; RecordType: TSQLRecordClass=nil);
  24036. var R: TSQLRecord;
  24037. Row: PPUtf8Char;
  24038. i: integer;
  24039. begin
  24040. if DestList=nil then
  24041. exit;
  24042. DestList.Clear;
  24043. if (self=nil) or (fRowCount=0) then
  24044. exit;
  24045. if RecordType=nil then begin
  24046. RecordType := QueryRecordType;
  24047. if RecordType=nil then
  24048. exit;
  24049. end;
  24050. R := RecordType.Create;
  24051. try
  24052. R.FillPrepare(self);
  24053. DestList.Count := fRowCount; // faster than manual Add()
  24054. Row := @fResults[FieldCount]; // Row^ points to first row of data
  24055. for i := 0 to fRowCount-1 do begin // TObjectList will free each instance
  24056. DestList.List[i] := RecordType.Create;
  24057. R.fFill.Fill(pointer(Row),TSQLRecord(DestList.List[i]));
  24058. Inc(Row,FieldCount); // next data row
  24059. end;
  24060. finally
  24061. R.Free;
  24062. end;
  24063. end;
  24064. function TSQLTable.ToObjArray(var ObjArray; RecordType: TSQLRecordClass=nil): boolean;
  24065. var R: TSQLRecord;
  24066. Row: PPUtf8Char;
  24067. i: integer;
  24068. arr: array of TSQLRecord absolute ObjArray;
  24069. begin
  24070. result := false;
  24071. ObjArrayClear(ObjArray);
  24072. if self=nil then
  24073. exit;
  24074. if RecordType=nil then begin
  24075. RecordType := QueryRecordType;
  24076. if RecordType=nil then
  24077. exit;
  24078. end;
  24079. result := true;
  24080. if fRowCount=0 then
  24081. exit;
  24082. R := RecordType.Create;
  24083. try
  24084. R.FillPrepare(self);
  24085. SetLength(arr,fRowCount); // faster than manual Add()
  24086. Row := @fResults[FieldCount]; // Row^ points to first row of data
  24087. for i := 0 to fRowCount-1 do begin
  24088. arr[i] := RecordType.Create;
  24089. R.fFill.Fill(pointer(Row),arr[i]);
  24090. Inc(Row,FieldCount); // next data row
  24091. end;
  24092. finally
  24093. R.Free;
  24094. end;
  24095. end;
  24096. function TSQLTable.ToObjectList(RecordType: TSQLRecordClass=nil): TObjectList;
  24097. begin
  24098. result := TObjectList.Create;
  24099. ToObjectList(result,RecordType);
  24100. end;
  24101. function TSQLTable.Step(SeekFirst: boolean=false; RowVariant: PVariant=nil): boolean;
  24102. begin
  24103. result := false;
  24104. if (self=nil) or (fRowCount<=0) then
  24105. exit; // nothing to iterate over
  24106. if SeekFirst then
  24107. fStepRow := 1 else
  24108. if fStepRow>=fRowCount then
  24109. exit else
  24110. inc(fStepRow);
  24111. result := true;
  24112. {$ifndef NOVARIANTS}
  24113. if RowVariant=nil then
  24114. exit;
  24115. if SQLTableRowVariantType=nil then
  24116. SQLTableRowVariantType := SynRegisterCustomVariantType(TSQLTableRowVariant);
  24117. if (PVarData(RowVariant)^.VType=SQLTableRowVariantType.VarType) and
  24118. (PSQLTableRowVariantData(RowVariant)^.VTable=self) and
  24119. (PSQLTableRowVariantData(RowVariant)^.VRow<0) then
  24120. exit; // already initialized -> quick exit
  24121. VarClear(RowVariant^);
  24122. PSQLTableRowVariantData(RowVariant)^.VType := SQLTableRowVariantType.VarType;
  24123. PSQLTableRowVariantData(RowVariant)^.VTable := self;
  24124. PSQLTableRowVariantData(RowVariant)^.VRow := -1; // follow fStepRow
  24125. {$endif NOVARIANTS}
  24126. end;
  24127. function TSQLTable.FieldBuffer(FieldIndex: Integer): PUTF8Char;
  24128. begin
  24129. if (self=nil) or (cardinal(FieldIndex)>=cardinal(fFieldCount)) then
  24130. raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): invalid index',
  24131. [self,FieldIndex]);
  24132. if (fStepRow=0) or (fStepRow>fRowCount) then
  24133. raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): no previous Step',
  24134. [self,FieldIndex]);
  24135. result := fResults[fStepRow*FieldCount+FieldIndex];
  24136. end;
  24137. function TSQLTable.FieldBuffer(const FieldName: RawUTF8): PUTF8Char;
  24138. var i: integer;
  24139. begin
  24140. i := FieldIndex(FieldName);
  24141. if i<0 then
  24142. raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): unknown field',
  24143. [self,FieldName]);
  24144. if (fStepRow=0) or (fStepRow>fRowCount) then
  24145. raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): no previous Step',
  24146. [self,FieldName]);
  24147. result := fResults[fStepRow*FieldCount+i];
  24148. end;
  24149. function TSQLTable.FieldAsInteger(FieldIndex: Integer): Int64;
  24150. begin
  24151. SetInt64(FieldBuffer(FieldIndex),result);
  24152. end;
  24153. function TSQLTable.FieldAsInteger(const FieldName: RawUTF8): Int64;
  24154. begin
  24155. SetInt64(FieldBuffer(FieldName),result);
  24156. end;
  24157. function TSQLTable.FieldAsFloat(FieldIndex: Integer): TSynExtended;
  24158. begin
  24159. result := GetExtended(FieldBuffer(FieldIndex));
  24160. end;
  24161. function TSQLTable.FieldAsFloat(const FieldName: RawUTF8): TSynExtended;
  24162. begin
  24163. result := GetExtended(FieldBuffer(FieldName));
  24164. end;
  24165. {$ifndef NOVARIANTS}
  24166. function TSQLTable.Field(FieldIndex: integer): variant;
  24167. begin
  24168. if (self=nil) or (cardinal(FieldIndex)>=cardinal(fFieldCount)) then
  24169. raise ESQLTableException.CreateUTF8('%.Field(%): invalid index',
  24170. [self,FieldIndex]);
  24171. if (fStepRow=0) or (fStepRow>fRowCount) then
  24172. raise ESQLTableException.CreateUTF8('%.Field(%): no previous Step',
  24173. [self,FieldIndex]);
  24174. GetVariant(fStepRow,FieldIndex,nil,result);
  24175. end;
  24176. function TSQLTable.Field(const FieldName: RawUTF8): variant;
  24177. var i: integer;
  24178. begin
  24179. i := FieldIndex(FieldName);
  24180. if i<0 then
  24181. raise ESQLTableException.CreateUTF8('%.Field(%): unknown field',
  24182. [self,FieldName]);
  24183. result := Field(i);
  24184. end;
  24185. {$endif}
  24186. function TSQLTable.CalculateFieldLengthMean(var aResult: TIntegerDynArray;
  24187. FromDisplay: boolean=false): integer;
  24188. procedure CalculateEnumerates(F: integer; P: PEnumType);
  24189. var R, i, n: integer;
  24190. EnumCounts: array of integer; // slow GetCaption() will be called once
  24191. U: PPUTF8Char;
  24192. begin
  24193. if P=nil then
  24194. exit; // no a true enumerate field
  24195. // 1. count of every possible enumerated value into EnumCounts[]
  24196. SetLength(EnumCounts,P^.MaxValue+1);
  24197. U := @fResults[FieldCount+F]; // start reading after first Row (= Field Names)
  24198. for R := 1 to fRowCount do begin
  24199. n := GetInteger(U^);
  24200. if n<=P^.MaxValue then
  24201. // update count of every enumerated value
  24202. inc(EnumCounts[n]) else
  24203. // GetCaption(invalid index) displays first one
  24204. inc(EnumCounts[0]);
  24205. inc(U,FieldCount); // points to next row
  24206. end;
  24207. // 2. update aResult[F] with displayed caption text length
  24208. n := 0;
  24209. for i := 0 to P^.MaxValue do
  24210. if EnumCounts[i]<>0 then
  24211. inc(n,length(P^.GetCaption(i))*EnumCounts[i]);
  24212. aResult[F] := n; // store displayed total length
  24213. end;
  24214. var R,F,n: integer;
  24215. U: PPUTF8Char;
  24216. Tot: cardinal;
  24217. begin
  24218. SetLength(aResult,FieldCount);
  24219. if FromDisplay and (length(fFieldLengthMean)=FieldCount) then begin
  24220. MoveFast(fFieldLengthMean[0],aResult[0],FieldCount*sizeof(integer));
  24221. result := fFieldLengthMeanSum;
  24222. exit;
  24223. end;
  24224. if fRowCount=0 then begin
  24225. // no data: calculate field length from first row (i.e. Field Names)
  24226. U := @fResults[0];
  24227. for F := 0 to FieldCount-1 do begin
  24228. inc(aResult[F],Utf8FirstLineToUnicodeLength(U^)); // count
  24229. inc(U); // points to next value
  24230. end;
  24231. Tot := 1;
  24232. end else begin
  24233. if not Assigned(fFieldType) then
  24234. InitFieldTypes;
  24235. U := @fResults[FieldCount]; // start reading after first Row
  24236. for R := 1 to fRowCount do // sum all lengths by field
  24237. for F := 0 to FieldCount-1 do begin
  24238. case fFieldType[F].ContentType of
  24239. sftInteger, sftBlob, sftBlobCustom, sftUTF8Custom, sftRecord,
  24240. sftRecordVersion, sftID, sftTID, sftSet, sftCurrency:
  24241. inc(aResult[F],8);
  24242. else inc(aResult[F],Utf8FirstLineToUnicodeLength(U^));
  24243. end;
  24244. inc(U); // points to next value
  24245. end;
  24246. if Assigned(fQueryTables) then begin
  24247. // aResult[] must be recalculated from captions, if exists
  24248. for F := 0 to FieldCount-1 do
  24249. with fFieldType[F] do
  24250. case ContentType of
  24251. sftEnumerate:
  24252. CalculateEnumerates(F,ContentTypeInfo);
  24253. end;
  24254. end;
  24255. Tot := fRowCount;
  24256. end;
  24257. result := 0;
  24258. for F := 0 to FieldCount-1 do begin
  24259. n := cardinal(aResult[F]) div Tot; // Mean = total/count
  24260. if n=0 then n := 1; // none should be 0
  24261. aResult[F] := n;
  24262. inc(result,n); // fast calculate mean sum
  24263. end;
  24264. end;
  24265. function TSQLTable.FieldLengthMean(Field: integer): cardinal;
  24266. begin
  24267. if (self=nil) or (cardinal(Field)>=cardinal(FieldCount)) or (fResults=nil) then
  24268. result := 0 else begin
  24269. if fFieldLengthMean=nil then
  24270. // if not already calculated, do it now
  24271. fFieldLengthMeanSum := CalculateFieldLengthMean(fFieldLengthMean);
  24272. result := fFieldLengthMean[Field];
  24273. end;
  24274. end;
  24275. function TSQLTable.FieldLengthMeanSum: cardinal;
  24276. begin
  24277. if self=nil then
  24278. result := 0 else begin
  24279. if fFieldLengthMean=nil then
  24280. FieldLengthMean(0); // initialize fFieldLengthMean[] and fFieldLengthMeanSum
  24281. result := fFieldLengthMeanSum;
  24282. end;
  24283. end;
  24284. function TSQLTable.FieldLengthMax(Field: integer; NeverReturnsZero: boolean): cardinal;
  24285. var i: integer;
  24286. len: cardinal;
  24287. U: PPUTF8Char;
  24288. begin
  24289. result := 0;
  24290. if (self<>nil) and (cardinal(Field)<cardinal(FieldCount)) then begin
  24291. if not Assigned(fFieldType) then
  24292. InitFieldTypes;
  24293. with fFieldType[Field] do
  24294. if ContentSize>=0 then
  24295. // return already computed value
  24296. result := ContentSize else begin
  24297. if (ContentTypeInfo<>nil) and (ContentType=sftEnumerate) then begin
  24298. // compute maximum size from available captions
  24299. for i := 0 to PEnumType(ContentTypeInfo)^.MaxValue do begin
  24300. len := length(PEnumType(ContentTypeInfo)^.GetCaption(i));
  24301. if len>result then
  24302. result := len;
  24303. end;
  24304. end else begin
  24305. // compute by reading all data rows
  24306. U := @fResults[FieldCount+Field];
  24307. for i := 1 to fRowCount do begin
  24308. len := StrLen(U^);
  24309. if len>result then
  24310. result := len;
  24311. inc(U,FieldCount);
  24312. end;
  24313. end;
  24314. ContentSize := result;
  24315. end;
  24316. end;
  24317. if (result=0) and NeverReturnsZero then
  24318. result := 1; // minimal not null length
  24319. end;
  24320. function TSQLTable.FieldTable(Field: integer): TSQLRecordClass;
  24321. begin
  24322. if (self=nil) or (cardinal(Field)>=cardinal(FieldCount)) or (fQueryTables=nil) then
  24323. result := nil else begin
  24324. if not Assigned(fFieldType) then
  24325. InitFieldTypes;
  24326. Field := fFieldType[Field].TableIndex;
  24327. if Field<0 then
  24328. result := nil else
  24329. result := fQueryTables[Field];
  24330. end;
  24331. end;
  24332. procedure TSQLTable.SetFieldLengthMean(const Lengths: array of cardinal);
  24333. var F: integer;
  24334. n: cardinal;
  24335. begin
  24336. if (self=nil) or (length(Lengths)<>FieldCount) then
  24337. exit;
  24338. if fFieldLengthMean=nil then // if not already calculated, allocate array
  24339. SetLength(fFieldLengthMean,FieldCount);
  24340. fFieldLengthMeanSum := 0;
  24341. for F := 0 to FieldCount-1 do begin
  24342. n := Lengths[F];
  24343. if n=0 then n := 1; // none should be 0
  24344. fFieldLengthMean[F] := n;
  24345. inc(fFieldLengthMeanSum,n); // fast calculate mean sum
  24346. end;
  24347. end;
  24348. procedure TSQLTable.FieldLengthMeanIncrease(aField, aIncrease: integer);
  24349. begin
  24350. if (self=nil) or (cardinal(aField)>=cardinal(FieldCount)) then
  24351. exit; // avoid GPF
  24352. if fFieldLengthMean=nil then
  24353. FieldLengthMean(0); // initialize fFieldLengthMean[] and fFieldLengthMeanSum
  24354. inc(fFieldLengthMean[aField],aIncrease);
  24355. inc(fFieldLengthMeanSum,aIncrease);
  24356. end;
  24357. function TSQLTable.SearchValue(const aUpperValue: RawUTF8;
  24358. StartRow, FieldIndex: integer; Client: TObject; Lang: TSynSoundExPronunciation;
  24359. UnicodeComparison: boolean): integer;
  24360. var U: PPUTF8Char;
  24361. Kind: TSQLFieldType;
  24362. Search: PAnsiChar;
  24363. UpperUnicode: RawUnicode;
  24364. UpperUnicodeLen: integer;
  24365. EnumType: PEnumType;
  24366. Val64: Int64;
  24367. i,err: integer;
  24368. EnumValue: RawUTF8;
  24369. s: string;
  24370. P: PShortString;
  24371. EnumValues: set of 0..63;
  24372. Soundex: TSynSoundEx;
  24373. CL: TSQLRest absolute Client;
  24374. tmp: array[0..23] of AnsiChar;
  24375. begin
  24376. result := 0;
  24377. if (self=nil) or (StartRow<=0) or (StartRow>fRowCount) or (aUpperValue='') or
  24378. (cardinal(FieldIndex)>=cardinal(FieldCount)) then
  24379. exit;
  24380. Search := pointer(aUpperValue);
  24381. if Search^='%' then begin
  24382. inc(Search);
  24383. if Search^='%' then begin
  24384. inc(Search);
  24385. if Search^='%' then begin
  24386. inc(Search);
  24387. Lang := sndxSpanish;
  24388. end else
  24389. Lang := sndxFrench;
  24390. end else
  24391. Lang := sndxEnglish;
  24392. end;
  24393. if ((Lang<>sndxNone) and not Soundex.Prepare(Search,Lang)) then
  24394. exit;
  24395. result := StartRow;
  24396. Kind := FieldType(FieldIndex,@EnumType);
  24397. U := @fResults[FieldCount*StartRow+FieldIndex];
  24398. // search in one specified field value
  24399. if (Kind=sftEnumerate) and (EnumType<>nil) then begin
  24400. // for enumerates: first search in all available values
  24401. Int64(EnumValues) := 0;
  24402. P := @EnumType^.NameList;
  24403. for i := 0 to EnumType^.MaxValue do begin
  24404. EnumValue := TrimLeftLowerCaseShort(P);
  24405. GetCaptionFromPCharLen(pointer(EnumValue),s);
  24406. StringToUTF8(s,EnumValue);
  24407. if ((Lang<>sndxNone) and SoundEx.UTF8(pointer(EnumValue))) or
  24408. ((Lang=sndxNone) and FindUTF8(pointer(EnumValue),Search)) then
  24409. include(EnumValues,i);
  24410. inc(PByte(P),ord(P^[0])+1);
  24411. // {$ifdef FPC}P := AlignToPtr(P);{$endif} enum values seem to be not aligned
  24412. end;
  24413. // then search directly from the INTEGER value
  24414. if Int64(EnumValues)<>0 then
  24415. while cardinal(result)<=cardinal(fRowCount) do begin
  24416. i := GetInteger(U^,err);
  24417. if (err=0) and (i in EnumValues) then
  24418. exit; // we found a matching field
  24419. inc(U,FieldCount); // ignore all other fields -> jump to next row data
  24420. inc(Result);
  24421. end;
  24422. result := 0; // not found
  24423. exit;
  24424. end;
  24425. // special cases: conversion from INTEGER to text before search
  24426. if Kind in [sftTimeLog,sftModTime,sftCreateTime] then
  24427. while cardinal(result)<=cardinal(fRowCount) do begin
  24428. SetInt64(U^,Val64);
  24429. if Val64<>0 then begin
  24430. tmp[TTimeLogBits(Val64).Text(tmp,true,' ')] := #0;
  24431. if FindAnsi(tmp,Search) then
  24432. exit;
  24433. end;
  24434. inc(U,FieldCount); // ignore all other fields -> jump to next row data
  24435. inc(Result);
  24436. end
  24437. else
  24438. if ((Kind in [sftRecord,sftID,sftTID,sftSessionUserID]) and
  24439. (Client<>nil) and Client.InheritsFrom(TSQLRest) and (CL.Model<>nil)) then
  24440. while cardinal(result)<=cardinal(fRowCount) do begin
  24441. SetInt64(U^,Val64);
  24442. if Val64<>0 then begin
  24443. if Kind=sftRecord then
  24444. EnumValue := RecordRef(Val64).Text(CL.Model) else
  24445. EnumValue := U^; // sftID/sftTID -> display ID number -> no sounded
  24446. if Lang=sndxNone then begin
  24447. if FindUTF8(pointer(EnumValue),Search) then exit;
  24448. end else
  24449. if SoundEx.UTF8(pointer(EnumValue)) then exit;
  24450. end;
  24451. inc(U,FieldCount); // ignore all other fields -> jump to next row data
  24452. inc(Result);
  24453. end
  24454. else
  24455. // by default, search as UTF-8 encoded text
  24456. if Lang<>sndxNone then begin
  24457. while cardinal(result)<=cardinal(fRowCount) do
  24458. if SoundEx.UTF8(U^) then
  24459. exit else begin
  24460. inc(U,FieldCount); // ignore all other fields -> jump to next row data
  24461. inc(Result);
  24462. end;
  24463. end else
  24464. if UnicodeComparison then begin
  24465. // slowest but always accurate Unicode comparison
  24466. UpperUnicode := UTF8DecodeToRawUnicodeUI(RawUTF8(Search),@UpperUnicodeLen);
  24467. while cardinal(result)<=cardinal(fRowCount) do
  24468. if FindUnicode(pointer(Utf8DecodeToRawUnicode(U^,0)),
  24469. pointer(UpperUnicode),UpperUnicodeLen) then
  24470. exit else begin
  24471. inc(U,FieldCount); // ignore all other fields -> jump to next row data
  24472. inc(Result);
  24473. end
  24474. end else
  24475. // default fast Win1252 search
  24476. while cardinal(result)<=cardinal(fRowCount) do
  24477. if FindUTF8(U^,Search) then
  24478. exit else begin
  24479. inc(U,FieldCount); // ignore all other fields -> jump to next row data
  24480. inc(Result);
  24481. end;
  24482. result := 0; // not found
  24483. end;
  24484. function TSQLTable.SearchValue(const aUpperValue: RawUTF8;
  24485. StartRow: integer; FieldIndex: PInteger; Client: TObject; Lang: TSynSoundExPronunciation;
  24486. UnicodeComparison: boolean): integer;
  24487. var F, Row: integer;
  24488. begin
  24489. result := 0;
  24490. if (self=nil) or (StartRow<=0) or (StartRow>fRowCount) or (aUpperValue='') then
  24491. exit;
  24492. // search in all fields values
  24493. for F := 0 to FieldCount-1 do begin
  24494. Row := SearchValue(aUpperValue,StartRow,F,Client,Lang,UnicodeComparison);
  24495. if (Row<>0) and ((result=0) or (Row<result)) then begin
  24496. if FieldIndex<>nil then
  24497. FieldIndex^ := F;
  24498. result := Row;
  24499. end;
  24500. end;
  24501. end;
  24502. function TSQLTable.SearchFieldEquals(const aValue: RawUTF8; FieldIndex: integer): integer;
  24503. begin
  24504. result := 0;
  24505. if (self=nil) or (aValue='') or (cardinal(FieldIndex)>cardinal(fFieldCount)) then
  24506. exit;
  24507. for result := 1 to fRowCount do
  24508. if UTF8IComp(Get(result,FieldIndex),pointer(aValue))=0 then
  24509. exit;
  24510. result := 0;
  24511. end;
  24512. {$ifndef NOVARIANTS}
  24513. function TSQLTable.GetVariant(Row, Field: integer; Client: TObject): Variant;
  24514. begin
  24515. GetVariant(Row,Field,Client,result);
  24516. end;
  24517. procedure TSQLTable.GetVariant(Row,Field: integer; Client: TObject; var result: variant);
  24518. var aType: TSQLFieldType;
  24519. aTypeInfo: pointer;
  24520. begin
  24521. if Row=0 then // Field Name
  24522. RawUTF8ToVariant(GetU(0,Field),result) else begin
  24523. aType := FieldType(Field,@aTypeInfo);
  24524. ValueVarToVariant(Get(Row,Field),aType,TVarData(result),true,aTypeInfo);
  24525. end;
  24526. end;
  24527. function TSQLTable.GetValue(const aLookupFieldName,aLookupValue,aValueFieldName: RawUTF8): variant;
  24528. var f,r,v: integer;
  24529. begin
  24530. SetVariantNull(result);
  24531. f := FieldIndex(aLookupFieldName);
  24532. v := FieldIndex(aValueFieldName);
  24533. if (f<0) or (v<0) then
  24534. exit;
  24535. r := SearchFieldEquals(aLookupValue,f);
  24536. if r>0 then
  24537. GetVariant(r,v,nil,Result);
  24538. end;
  24539. {$endif NOVARIANTS}
  24540. function TSQLTable.ExpandAsString(Row, Field: integer; Client: TObject;
  24541. out Text: string; const CustomFormat: string): TSQLFieldType;
  24542. var EnumType: PEnumType;
  24543. err: integer;
  24544. Value: Int64;
  24545. Ref: RecordRef absolute Value;
  24546. label IsDateTime;
  24547. begin // Text was already forced to '' because was defined as "out" parameter
  24548. if Row=0 then begin // Field Name
  24549. result := sftUnknown;
  24550. Text := GetCaption(0,Field);
  24551. exit;
  24552. end;
  24553. result := FieldType(Field,@EnumType);
  24554. case result of
  24555. sftDateTime: begin
  24556. Value := Iso8601ToTimeLogPUTF8Char(Get(Row,Field),0);
  24557. IsDateTime:
  24558. if Value<>0 then begin
  24559. {$ifndef LVCL}
  24560. if CustomFormat<>'' then begin
  24561. Text := FormatDateTime(CustomFormat,TTimeLogBits(Value).ToDateTime);
  24562. if Text<>CustomFormat then
  24563. exit; // valid conversion
  24564. end;
  24565. {$endif LVCL}
  24566. Text := TTimeLogBits(Value).i18nText;
  24567. exit;
  24568. end;
  24569. end;
  24570. sftBlob:
  24571. Text := '???';
  24572. sftFloat:
  24573. if CustomFormat<>'' then
  24574. try
  24575. if pos('%',CustomFormat)>0 then
  24576. Text := Format(CustomFormat,[GetExtended(Get(Row,Field))])
  24577. {$ifndef LVCL} else
  24578. Text := FormatFloat(CustomFormat,GetExtended(Get(Row,Field)))
  24579. {$endif LVCL};
  24580. exit;
  24581. except
  24582. on {$ifdef LVCL}Exception{$else}EConvertError{$endif} do
  24583. Text := '';
  24584. end;
  24585. sftCurrency:
  24586. if CustomFormat<>'' then
  24587. try
  24588. if pos('%',CustomFormat)>0 then
  24589. Text := Format(CustomFormat,[StrToCurrency(Get(Row,Field))])
  24590. {$ifndef LVCL} else
  24591. Text := FormatCurr(CustomFormat,StrToCurrency(Get(Row,Field)))
  24592. {$endif};
  24593. exit;
  24594. except
  24595. on {$ifdef LVCL}Exception{$else}EConvertError{$endif} do
  24596. Text := '';
  24597. end;
  24598. sftEnumerate, sftSet, sftRecord, sftID, sftTID, sftRecordVersion, sftSessionUserID,
  24599. sftTimeLog, sftModTime, sftCreateTime: begin
  24600. Value := GetInt64(Get(Row,Field),err);
  24601. if err<>0 then
  24602. // not an integer -> to be displayed as sftUTF8Text
  24603. result := sftUTF8Text else
  24604. case result of
  24605. sftEnumerate: begin
  24606. Text := EnumType^.GetCaption(Value);
  24607. exit;
  24608. end;
  24609. sftTimeLog, sftModTime, sftCreateTime:
  24610. goto IsDateTime;
  24611. { sftID, sftTID, sftSet, sftRecordVersion:
  24612. result := sftUTF8Text; // will display INTEGER field as number }
  24613. sftRecord:
  24614. if (Value<>0) and
  24615. (Client<>nil) and Client.InheritsFrom(TSQLRest) then // 'TableName ID'
  24616. Text := {$ifdef UNICODE}Ansi7ToString{$endif}(Ref.Text(TSQLRest(Client).Model)) else
  24617. result := sftUTF8Text; // display ID number if no table model
  24618. end;
  24619. end;
  24620. end;
  24621. if Text='' then
  24622. // returns the value as text by default
  24623. Text := GetString(Row,Field);
  24624. end;
  24625. function TSQLTable.ExpandAsSynUnicode(Row,Field: integer; Client: TObject; out Text: SynUnicode): TSQLFieldType;
  24626. var s: string;
  24627. begin
  24628. result := ExpandAsString(Row,Field,Client,s);
  24629. Text := StringToSynUnicode(s);
  24630. end;
  24631. function TSQLTable.GetTimeLog(Row, Field: integer; Expanded: boolean;
  24632. FirstTimeChar: AnsiChar): RawUTF8;
  24633. var Value: TTimeLogBits;
  24634. begin
  24635. SetInt64(Get(Row,Field),Value.Value);
  24636. result := Value.Text(Expanded,FirstTimeChar);
  24637. end;
  24638. {$ifndef NOVARIANTS}
  24639. { TSQLTableRowVariant }
  24640. procedure TSQLTableRowVariant.IntGet(var Dest: TVarData;
  24641. const V: TVarData; Name: PAnsiChar);
  24642. var r,f: integer;
  24643. begin
  24644. if (TSQLTableRowVariantData(V).VTable=nil) or (Name=nil) then
  24645. ESQLTableException.CreateUTF8('Invalid %.% call',[self,Name]);
  24646. r := TSQLTableRowVariantData(V).VRow;
  24647. if r<0 then begin
  24648. r := TSQLTableRowVariantData(V).VTable.fStepRow;
  24649. if (r=0) or (r>TSQLTableRowVariantData(V).VTable.fRowCount) then
  24650. raise ESQLTableException.CreateUTF8('%.%: no previous Step',[self,Name]);
  24651. end;
  24652. f := TSQLTableRowVariantData(V).VTable.FieldIndex(PUTF8Char(Name));
  24653. if cardinal(f)>=cardinal(TSQLTableRowVariantData(V).VTable.fFieldCount) then
  24654. raise ESQLTableException.CreateUTF8('%.%: unknown field',[self,Name]);
  24655. TSQLTableRowVariantData(V).VTable.GetVariant(r,f,nil,Variant(Dest));
  24656. end;
  24657. procedure TSQLTableRowVariant.IntSet(const V, Value: TVarData; Name: PAnsiChar);
  24658. begin
  24659. ESQLTableException.CreateUTF8('% is read-only',[self]);
  24660. end;
  24661. procedure TSQLTableRowVariant.Cast(var Dest: TVarData; const Source: TVarData);
  24662. begin
  24663. CastTo(Dest,Source,VarType);
  24664. end;
  24665. procedure TSQLTableRowVariant.CastTo(var Dest: TVarData;
  24666. const Source: TVarData; const AVarType: TVarType);
  24667. var r: integer;
  24668. tmp: variant; // use a temporary TDocVariant for the conversion
  24669. begin
  24670. if AVarType=VarType then begin
  24671. RaiseCastError;
  24672. end else begin
  24673. if Source.VType<>VarType then
  24674. RaiseCastError;
  24675. r := TSQLTableRowVariantData(Source).VRow;
  24676. if r<0 then
  24677. r := TSQLTableRowVariantData(Source).VTable.fStepRow;
  24678. TSQLTableRowVariantData(Source).VTable.ToDocVariant(r,tmp);
  24679. RawUTF8ToVariant(VariantSaveJSON(tmp),Dest,AVarType);
  24680. end;
  24681. end;
  24682. procedure TSQLTableRowVariant.ToJSON(W: TTextWriter; const Value: variant;
  24683. Escape: TTextWriterKind);
  24684. var r: integer;
  24685. tmp: variant; // write row via a TDocVariant
  24686. begin
  24687. r := TSQLTableRowVariantData(Value).VRow;
  24688. if r<0 then
  24689. r := TSQLTableRowVariantData(Value).VTable.fStepRow;
  24690. TSQLTableRowVariantData(Value).VTable.ToDocVariant(r,tmp);
  24691. W.AddVariant(tmp,Escape);
  24692. end;
  24693. {$endif NOVARIANTS}
  24694. procedure Base64MagicToBlob(Base64: PUTF8Char; var result: RawUTF8);
  24695. begin
  24696. // do not escape the result: returns e.g. X'53514C697465'
  24697. result := TSQLRawBlobToBlob(Base64ToBin(PAnsiChar(Base64),StrLen(Base64)));
  24698. end;
  24699. { TJSONObjectDecoder }
  24700. const
  24701. EndOfJSONField = [',',']','}',':'];
  24702. function GetJSONArrayOrObject(P: PUTF8Char; out PDest: PUTF8Char;
  24703. EndOfObject: PUTF8Char): RawUTF8;
  24704. var Beg: PUTF8Char;
  24705. begin
  24706. PDest := nil;
  24707. Beg := P;
  24708. P := GotoNextJSONObjectOrArray(P); // quick go to end of array of object
  24709. if P=nil then begin
  24710. result := '';
  24711. exit;
  24712. end;
  24713. if EndOfObject<>nil then
  24714. EndOfObject^ := P^;
  24715. PDest := P+1;
  24716. SetString(result,PAnsiChar(Beg),P-Beg);
  24717. end;
  24718. function GetJSONArrayOrObjectAsQuotedStr(P: PUTF8Char; out PDest: PUTF8Char;
  24719. EndOfObject: PUTF8Char): RawUTF8;
  24720. var Beg: PUTF8Char;
  24721. begin
  24722. result := '';
  24723. PDest := nil;
  24724. Beg := P;
  24725. P := GotoNextJSONObjectOrArray(P); // quick go to end of array of object
  24726. if P=nil then
  24727. exit;
  24728. if EndOfObject<>nil then
  24729. EndOfObject^ := P^;
  24730. P^ := #0; // so Beg will be a valid ASCIIZ string
  24731. PDest := P+1;
  24732. result := QuotedStr(Beg,'''');
  24733. end;
  24734. procedure TJSONObjectDecoder.Decode(var P: PUTF8Char; const Fields: TRawUTF8DynArray;
  24735. Params: TJSONObjectDecoderParams; const RowID: TID; ReplaceRowIDWithID: boolean);
  24736. var EndOfObject: AnsiChar;
  24737. procedure GetSQLValue(ndx: integer);
  24738. var wasString: boolean;
  24739. res: PUTF8Char;
  24740. c: integer;
  24741. begin
  24742. res := P;
  24743. if res=nil then begin
  24744. FieldValues[ndx] := ''; // avoid GPF, but will return invalid SQL
  24745. exit;
  24746. end;
  24747. while res^ in [#1..' '] do inc(res);
  24748. if (PInteger(res)^=NULL_LOW) and
  24749. (res[4] in [#0,#9,#10,#13,' ',',','}',']']) then begin
  24750. /// GetJSONField('null') returns '' -> check here to make a diff with '""'
  24751. FieldTypeApproximation[ndx] := ftaNull;
  24752. FieldValues[ndx] := 'null';
  24753. inc(res,4);
  24754. while res^ in [#1..' '] do inc(res);
  24755. if res^=#0 then
  24756. P := nil else begin
  24757. EndOfObject := res^;
  24758. res^ := #0;
  24759. P := res+1;
  24760. end;
  24761. end else begin
  24762. // first check if nested object or array
  24763. case res^ of // handle JSON {object} or [array] in P
  24764. '{': begin // will work e.g. for custom variant types
  24765. FieldTypeApproximation[ndx] := ftaObject;
  24766. if params=pNonQuoted then
  24767. FieldValues[ndx] := GetJSONArrayOrObject(res,P,@EndOfObject) else
  24768. FieldValues[ndx] := GetJSONArrayOrObjectAsQuotedStr(res,P,@EndOfObject);
  24769. end;
  24770. '[': begin // will work e.g. for custom variant types
  24771. FieldTypeApproximation[ndx] := ftaArray;
  24772. if params=pNonQuoted then
  24773. FieldValues[ndx] := GetJSONArrayOrObject(res,P,@EndOfObject) else
  24774. FieldValues[ndx] := GetJSONArrayOrObjectAsQuotedStr(res,P,@EndOfObject);
  24775. end;
  24776. else begin
  24777. // handle JSON string, number or false/true in P
  24778. res := GetJSONField(res,P,@wasString,@EndOfObject);
  24779. if wasString then begin
  24780. c := PInteger(res)^ and $00ffffff;
  24781. if c=JSON_BASE64_MAGIC then begin
  24782. FieldTypeApproximation[ndx] := ftaBlob;
  24783. case Params of
  24784. pInlined: // untouched -> recognized as BLOB in SQLParamContent()
  24785. QuotedStr(res,'''',FieldValues[ndx]);
  24786. { pQuoted: // \uFFF0base64encodedbinary -> 'X''hexaencodedbinary'''
  24787. // if not inlined, it can be used directly in INSERT/UPDATE statements
  24788. Base64MagicToBlob(res+3,FieldValues[ndx]);
  24789. pNonQuoted:}
  24790. else // returned directly as RawByteString
  24791. FieldValues[ndx] := Base64ToBin(res+3);
  24792. end;
  24793. end else begin
  24794. if c=JSON_SQLDATE_MAGIC then begin
  24795. FieldTypeApproximation[ndx] := ftaDate;
  24796. inc(res,3); // ignore \uFFF1 magic marker
  24797. end else
  24798. FieldTypeApproximation[ndx] := ftaString;
  24799. // regular string content
  24800. if Params=pNonQuoted then
  24801. // returned directly as RawUTF8
  24802. SetString(FieldValues[ndx],PAnsiChar(res),StrLen(res)) else
  24803. { escape SQL strings, cf. the official SQLite3 documentation:
  24804. "A string is formed by enclosing the string in single quotes (').
  24805. A single quote within the string can be encoded by putting two
  24806. single quotes in a row - as in Pascal." }
  24807. QuotedStr(res,'''',FieldValues[ndx]);
  24808. end;
  24809. end else
  24810. if res=nil then begin
  24811. FieldValues[ndx] := ''; // avoid GPF, but will return invalid SQL
  24812. exit;
  24813. end else
  24814. // non string params (numeric or false/true) are passed untouched
  24815. if PInteger(res)^=FALSE_LOW then begin
  24816. FieldValues[ndx] := '0';
  24817. FieldTypeApproximation[ndx] := ftaBoolean;
  24818. end else
  24819. if PInteger(res)^=TRUE_LOW then begin
  24820. FieldValues[ndx] := '1';
  24821. FieldTypeApproximation[ndx] := ftaBoolean;
  24822. end else begin
  24823. FieldValues[ndx] := res;
  24824. FieldTypeApproximation[ndx] := ftaNumber;
  24825. end;
  24826. end;
  24827. end;
  24828. end;
  24829. end;
  24830. var FN: PUTF8Char;
  24831. F: integer;
  24832. FieldIsRowID: Boolean;
  24833. begin
  24834. FieldCount := 0;
  24835. DecodedRowID := 0;
  24836. FillcharFast(FieldTypeApproximation,sizeof(FieldTypeApproximation),0);
  24837. InlinedParams := Params=pInlined;
  24838. if pointer(Fields)=nil then begin
  24839. // get "COL1"="VAL1" pairs, stopping at '}' or ']'
  24840. DecodedFieldNames := @FieldNames;
  24841. if RowID>0 then begin // insert explicit RowID
  24842. if ReplaceRowIDWithID then
  24843. FieldNames[0] := 'ID' else
  24844. FieldNames[0] := 'RowID';
  24845. FieldValues[0] := Int64ToUtf8(RowID); // Int64ToUtf8(RowID,FieldValues[0]) fails on D2007
  24846. FieldCount := 1;
  24847. DecodedRowID := RowID;
  24848. end;
  24849. repeat
  24850. if P=nil then
  24851. break;
  24852. FN := GetJSONPropName(P);
  24853. if (FN=nil) or (P=nil) then
  24854. break; // invalid JSON field name
  24855. FieldIsRowID := IsRowId(FN);
  24856. if FieldIsRowID then
  24857. if RowID>0 then begin
  24858. GetJSONField(P,P,nil,@EndOfObject); // ignore this if explicit RowID
  24859. if EndOfObject in [#0,'}',']'] then
  24860. break else continue;
  24861. end else
  24862. if ReplaceRowIDWithID then
  24863. FN := 'ID';
  24864. SetString(FieldNames[FieldCount],PAnsiChar(FN),StrLen(FN));
  24865. GetSQLValue(FieldCount); // update EndOfObject
  24866. if FieldIsRowID then
  24867. SetID(FieldValues[FieldCount],DecodedRowID);
  24868. inc(FieldCount);
  24869. if FieldCount=MAX_SQLFIELDS then
  24870. raise EParsingException.Create('Too many inlines in TJSONObjectDecoder');
  24871. until EndOfObject in [#0,'}',']'];
  24872. end else begin
  24873. // get "VAL1","VAL2"...
  24874. if P=nil then
  24875. exit;
  24876. if RowID>0 then
  24877. raise EParsingException.Create('TJSONObjectDecoder(expanded) won''t handle RowID');
  24878. if length(Fields)>MAX_SQLFIELDS then
  24879. raise EParsingException.Create('Too many inlines in TJSONObjectDecoder');
  24880. DecodedFieldNames := pointer(Fields);
  24881. FieldCount := length(Fields);
  24882. for F := 0 to FieldCount-1 do
  24883. GetSQLValue(F); // update EndOfObject
  24884. end;
  24885. end;
  24886. procedure TJSONObjectDecoder.Decode(const JSON: RawUTF8; const Fields: TRawUTF8DynArray;
  24887. Params: TJSONObjectDecoderParams; const RowID: TID=0; ReplaceRowIDWithID: Boolean=false);
  24888. var tmp: TSynTempBuffer;
  24889. P: PUTF8Char;
  24890. begin
  24891. tmp.Init(JSON);
  24892. try
  24893. P := tmp.buf;
  24894. if P<>nil then
  24895. while P^ in [#1..' ','{','['] do inc(P);
  24896. Decode(P,Fields,Params,RowID,ReplaceRowIDWithID);
  24897. finally
  24898. tmp.Done;
  24899. end;
  24900. end;
  24901. function TJSONObjectDecoder.SameFieldNames(const Fields: TRawUTF8DynArray): boolean;
  24902. var i: integer;
  24903. begin
  24904. result := false;
  24905. if length(Fields)<>FieldCount then
  24906. exit;
  24907. for i := 0 to FieldCount-1 do
  24908. if not IdemPropNameU(Fields[i],FieldNames[i]) then
  24909. exit;
  24910. result := true;
  24911. end;
  24912. procedure TJSONObjectDecoder.AssignFieldNamesTo(var Fields: TRawUTF8DynArray);
  24913. var i: integer;
  24914. begin
  24915. SetLength(Fields,FieldCount);
  24916. for i := 0 to FieldCount-1 do
  24917. Fields[i] := FieldNames[i];
  24918. end;
  24919. function TJSONObjectDecoder.EncodeAsSQLPrepared(const TableName: RawUTF8;
  24920. Occasion: TSQLOccasion; const UpdateIDFieldName: RawUTF8;
  24921. BatchOptions: TSQLRestBatchOptions): RawUTF8;
  24922. var F: integer;
  24923. W: TTextWriter;
  24924. begin
  24925. W := TTextWriter.CreateOwnedStream(1024);
  24926. try
  24927. case Occasion of
  24928. soUpdate: begin
  24929. if FieldCount=0 then
  24930. raise EORMException.Create('Invalid EncodeAsSQLPrepared(0)');
  24931. W.AddShort('update ');
  24932. W.AddString(TableName);
  24933. W.AddShort(' set ');
  24934. for F := 0 to FieldCount-1 do begin // append 'COL1=?,COL2=?'
  24935. W.AddString(DecodedFieldNames^[F]);
  24936. W.AddShort('=?,');
  24937. end;
  24938. W.CancelLastComma;
  24939. W.AddShort(' where ');
  24940. W.AddString(UpdateIDFieldName);
  24941. W.Add('=','?');
  24942. end;
  24943. soInsert: begin
  24944. if boInsertOrIgnore in BatchOptions then
  24945. W.AddShort('insert or ignore into ') else
  24946. if boInsertOrReplace in BatchOptions then
  24947. W.AddShort('insert or replace into ') else
  24948. W.AddShort('insert into ');
  24949. W.AddString(TableName);
  24950. if FieldCount=0 then
  24951. W.AddShort(' default values') else begin
  24952. W.Add(' ','(');
  24953. for F := 0 to FieldCount-1 do begin // append 'COL1,COL2'
  24954. W.AddString(DecodedFieldNames^[F]);
  24955. W.Add(',');
  24956. end;
  24957. W.CancelLastComma;
  24958. W.AddShort(') values (');
  24959. W.AddStrings('?,',FieldCount);
  24960. W.CancelLastComma;
  24961. W.Add(')');
  24962. end;
  24963. end;
  24964. else
  24965. raise EORMException.Create('Invalid EncodeAsSQLPrepared() call');
  24966. end;
  24967. W.SetText(result);
  24968. finally
  24969. W.Free;
  24970. end;
  24971. end;
  24972. function TJSONObjectDecoder.EncodeAsSQL(Update: boolean): RawUTF8;
  24973. var F: integer;
  24974. W: TTextWriter;
  24975. procedure AddValue;
  24976. begin
  24977. if InlinedParams then
  24978. W.AddShort(':(');
  24979. W.AddString(FieldValues[F]);
  24980. if InlinedParams then
  24981. W.AddShort('):,') else
  24982. W.Add(',');
  24983. end;
  24984. begin
  24985. result := '';
  24986. if FieldCount=0 then
  24987. exit;
  24988. W := TTextWriter.CreateOwnedStream(2048);
  24989. try
  24990. if Update then begin
  24991. for F := 0 to FieldCount-1 do // append 'COL1=...,COL2=...'
  24992. if not IsRowID(pointer(DecodedFieldNames^[F])) then begin
  24993. W.AddString(DecodedFieldNames^[F]);
  24994. W.Add('=');
  24995. AddValue;
  24996. end;
  24997. W.CancelLastComma;
  24998. end else begin // returns ' (COL1,COL2) VALUES ('VAL1',VAL2)'
  24999. W.Add(' ','(');
  25000. for F := 0 to FieldCount-1 do begin // append 'COL1,COL2'
  25001. W.AddString(DecodedFieldNames^[F]);
  25002. W.Add(',');
  25003. end;
  25004. W.CancelLastComma;
  25005. W.AddShort(') VALUES (');
  25006. for F := 0 to FieldCount-1 do
  25007. AddValue;
  25008. W.CancelLastComma;
  25009. W.Add(')');
  25010. end;
  25011. W.SetText(result);
  25012. finally
  25013. W.Free;
  25014. end;
  25015. end;
  25016. procedure TJSONObjectDecoder.EncodeAsJSON(out result: RawUTF8);
  25017. var F: integer;
  25018. W: TTextWriter;
  25019. begin
  25020. if FieldCount=0 then
  25021. exit;
  25022. W := TTextWriter.CreateOwnedStream(2048);
  25023. try
  25024. W.Add('{');
  25025. for F := 0 to FieldCount-1 do begin
  25026. W.AddFieldName(DecodedFieldNames^[F]);
  25027. W.AddQuotedStringAsJSON(FieldValues[F]);
  25028. W.Add(',');
  25029. end;
  25030. W.CancelLastComma;
  25031. W.Add('}');
  25032. W.SetText(result);
  25033. finally
  25034. W.Free;
  25035. end;
  25036. end;
  25037. function TJSONObjectDecoder.FindFieldName(const FieldName: RawUTF8): integer;
  25038. begin
  25039. for result := 0 to FieldCount-1 do
  25040. if IdemPropNameU(FieldNames[result],FieldName) then
  25041. exit;
  25042. result := -1;
  25043. end;
  25044. procedure TJSONObjectDecoder.AddFieldValue(const FieldName,FieldValue: RawUTF8;
  25045. FieldType: TJSONObjectDecoderFieldType);
  25046. begin
  25047. if FieldCount=MAX_SQLFIELDS then
  25048. raise EParsingException.CreateUTF8(
  25049. 'Too many fields for TJSONObjectDecoder.AddField(%)',[FieldName]);
  25050. FieldNames[FieldCount] := FieldName;
  25051. FieldValues[FieldCount] := FieldValue;
  25052. FieldTypeApproximation[FieldCount] := FieldType;
  25053. inc(FieldCount);
  25054. end;
  25055. const
  25056. FROMINLINED: array[boolean] of TJSONObjectDecoderParams = (
  25057. pQuoted, pInlined);
  25058. function GetJSONObjectAsSQL(var P: PUTF8Char; const Fields: TRawUTF8DynArray;
  25059. Update, InlinedParams: boolean; RowID: TID=0; ReplaceRowIDWithID: Boolean=false): RawUTF8;
  25060. var Decoder: TJSONObjectDecoder;
  25061. begin
  25062. Decoder.Decode(P,Fields,FROMINLINED[InlinedParams],RowID,ReplaceRowIDWithID);
  25063. result := Decoder.EncodeAsSQL(Update);
  25064. end;
  25065. function GetJSONObjectAsSQL(const JSON: RawUTF8; Update, InlinedParams: boolean;
  25066. RowID: TID=0; ReplaceRowIDWithID: Boolean=false): RawUTF8; overload;
  25067. var Decoder: TJSONObjectDecoder;
  25068. begin
  25069. Decoder.Decode(JSON,nil,FROMINLINED[InlinedParams],RowID,ReplaceRowIDWithID);
  25070. result := Decoder.EncodeAsSQL(Update);
  25071. end;
  25072. function Expect(var P: PUTF8Char; const Value: RawUTF8): boolean;
  25073. {$ifdef HASINLINE}inline;{$endif}
  25074. var L: integer;
  25075. begin
  25076. if P=nil then
  25077. result := false else begin
  25078. while P^ in [#1..' '] do inc(P);
  25079. if Value='' then
  25080. result := false else begin
  25081. L := length(Value);
  25082. result := CompareMem(P,pointer(Value),L);
  25083. if result then
  25084. inc(P,L);
  25085. end;
  25086. end;
  25087. end;
  25088. function GetJSONIntegerVar(var P: PUTF8Char): PtrInt;
  25089. var c: PtrUInt;
  25090. begin
  25091. if P^ in [#1..' '] then repeat inc(P) until not(P^ in [#1..' ']);
  25092. c := byte(P^)-48;
  25093. if c>9 then
  25094. result := 0 else begin
  25095. result := c;
  25096. inc(P);
  25097. repeat
  25098. c := byte(P^)-48;
  25099. if c>9 then
  25100. break;
  25101. result := result*10+PtrInt(c);
  25102. inc(P);
  25103. until false;
  25104. end;
  25105. end;
  25106. function GetJSONInt64Var(var P: PUTF8Char): Int64;
  25107. var c: PtrUInt;
  25108. begin
  25109. if P^ in [#1..' '] then repeat inc(P) until not(P^ in [#1..' ']);
  25110. c := byte(P^)-48;
  25111. if c>9 then
  25112. result := 0 else begin
  25113. result := c;
  25114. inc(P);
  25115. repeat
  25116. c := byte(P^)-48;
  25117. if c>9 then
  25118. break;
  25119. result := result*10+Int64(c);
  25120. inc(P);
  25121. until false;
  25122. end;
  25123. end;
  25124. const
  25125. FIELDCOUNT_PATTERN: RawUTF8 = '{"fieldCount":';
  25126. ROWCOUNT_PATTERN: RawUTF8 = ',"rowCount":';
  25127. VALUES_PATTERN: RawUTF8 = ',"values":[';
  25128. function UnJSONFirstField(var P: PUTF8Char): RawUTF8;
  25129. // expand=true: [ {"col1":val11} ] -> val11
  25130. // expand=false: { "fieldCount":1,"values":["col1",val11] } -> vall11
  25131. begin
  25132. result := '';
  25133. if P=nil then exit;
  25134. if Expect(P,FIELDCOUNT_PATTERN) then begin
  25135. // not expanded format
  25136. if GetJSONIntegerVar(P)<>1 then
  25137. exit; // wrong field count
  25138. while P^<>'[' do if P^=#0 then exit else inc(P); // go to ["col1"
  25139. inc(P); // go to "col1"
  25140. end else begin
  25141. // expanded format
  25142. while P^<>'[' do if P^=#0 then exit else inc(P); // need an array of objects
  25143. repeat inc(P); if P^=#0 then exit; until P^='{'; // go to object begining
  25144. end;
  25145. GetJSONPropName(P); // ignore field name
  25146. result := GetJSONField(P,P); // get field value
  25147. end;
  25148. function IsNotAjaxJSON(P: PUTF8Char): Boolean;
  25149. begin
  25150. result := Expect(P,FIELDCOUNT_PATTERN);
  25151. end;
  25152. function NotExpandedBufferRowCountPos(P,PEnd: PUTF8Char): PUTF8Char;
  25153. var i: integer;
  25154. begin
  25155. result := nil;
  25156. if (PEnd<>nil) and (PEnd-P>24) then
  25157. for i := 1 to 24 do // search for "rowCount": at the end of the JSON buffer
  25158. case PEnd[-i] of
  25159. ']',',':
  25160. exit;
  25161. ':': begin
  25162. if CompareMem(PEnd-i-11,pointer(ROWCOUNT_PATTERN),11) then
  25163. result := PEnd-i+1;
  25164. exit;
  25165. end;
  25166. end;
  25167. end;
  25168. function IsNotExpandedBuffer(var P: PUTF8Char; PEnd: PUTF8Char;
  25169. var FieldCount,RowCount: integer): boolean;
  25170. procedure GetRowCountNotExpanded(P: PUTF8Char);
  25171. begin
  25172. RowCount := 0;
  25173. repeat
  25174. // get a row
  25175. P := GotoNextJSONItem(P,FieldCount);
  25176. if P=nil then exit; // unexpected end
  25177. inc(RowCount);
  25178. until P[-1]=']'; // end of array
  25179. if P^ in ['}',','] then begin // expected formated JSON stream
  25180. if RowCount>0 then
  25181. dec(RowCount); // first Row = field names -> data in rows 1..RowCount
  25182. end else
  25183. RowCount := -1; // bad format -> no data
  25184. end;
  25185. var RowCountPos: PUTF8Char;
  25186. begin
  25187. if not Expect(P,FIELDCOUNT_PATTERN) then begin
  25188. result := false;
  25189. exit;
  25190. end;
  25191. FieldCount := GetJSONIntegerVar(P);
  25192. if Expect(P,ROWCOUNT_PATTERN) then
  25193. RowCount := GetJSONIntegerVar(P) else begin
  25194. RowCountPos := NotExpandedBufferRowCountPos(P,PEnd);
  25195. if RowCountPos=nil then
  25196. RowCount := -1 else // mark "rowCount":.. not available
  25197. RowCount := GetCardinal(RowCountPos);
  25198. end;
  25199. result := (FieldCount<>0) and Expect(P,VALUES_PATTERN);
  25200. if result and (RowCount<0) then
  25201. GetRowCountNotExpanded(P); // returns RowCount=-1 if P^ is invalid
  25202. end;
  25203. function StartWithQuotedID(P: PUTF8Char; out ID: TID): boolean;
  25204. begin
  25205. if PCardinal(P)^ and $ffffdfdf=
  25206. ord('I')+ord('D')shl 8+ord('"')shl 16+ord(':')shl 24 then begin
  25207. SetID(P+4,ID);
  25208. result := true;
  25209. exit;
  25210. end else
  25211. if (PCardinalArray(P)^[0] and $dfdfdfdf=
  25212. ord('R')+ord('O')shl 8+ord('W')shl 16+ord('I')shl 24) and
  25213. (PCardinalArray(P)^[1] and $ffffdf=
  25214. ord('D')+ord('"')shl 8+ord(':')shl 16) then begin
  25215. SetID(P+7,ID);
  25216. result := true;
  25217. exit;
  25218. end;
  25219. ID := 0;
  25220. result := false;
  25221. end;
  25222. function StartWithID(P: PUTF8Char; out ID: TID): boolean;
  25223. begin
  25224. if PCardinal(P)^ and $ffdfdf=
  25225. ord('I')+ord('D')shl 8+ord(':')shl 16 then begin
  25226. SetID(P+3,ID);
  25227. result := true;
  25228. exit;
  25229. end else
  25230. if (PCardinalArray(P)^[0] and $dfdfdfdf=
  25231. ord('R')+ord('O')shl 8+ord('W')shl 16+ord('I')shl 24) and
  25232. (PCardinalArray(P)^[1] and $ffdf=ord('D')+ord(':')shl 8) then begin
  25233. SetID(P+6,ID);
  25234. result := true;
  25235. exit;
  25236. end;
  25237. ID := 0;
  25238. result := false;
  25239. end;
  25240. function JSONGetID(P: PUTF8Char; out ID: TID): Boolean;
  25241. begin
  25242. if (P<>nil) and
  25243. NextNotSpaceCharIs(P,'{') then
  25244. if NextNotSpaceCharIs(P,'"') then
  25245. result := StartWithQuotedID(P,ID) else
  25246. result := StartWithID(P,ID) else begin
  25247. ID := 0;
  25248. result := false;
  25249. end;
  25250. end;
  25251. function JSONGetObject(var P: PUTF8Char; ExtractID: PID;
  25252. var EndOfObject: AnsiChar; KeepIDField: boolean): RawUTF8;
  25253. var Beg, PC: PUTF8Char;
  25254. begin
  25255. result := '';
  25256. if P=nil then
  25257. exit;
  25258. if P^ in [#1..' '] then repeat inc(P) until not(P^ in [#1..' ']);
  25259. if P^<>'{' then
  25260. exit;
  25261. Beg := P;
  25262. P := GotoNextJSONObjectOrArray(Beg);
  25263. if (P<>nil) and not (P^ in EndOfJSONField) then
  25264. P := nil;
  25265. if P<>nil then begin
  25266. EndOfObject := P^;
  25267. inc(P); // ignore end of object, i.e. ',' or ']'
  25268. if ExtractID<>nil then
  25269. if JSONGetID(Beg,ExtractID^) and not KeepIDField then begin
  25270. PC := PosChar(Beg,','); // ignore the '"ID":203,' pair
  25271. PC^ := '{';
  25272. SetString(result,PAnsiChar(PC),P-PC-1);
  25273. exit;
  25274. end;
  25275. SetString(result,PAnsiChar(Beg),P-Beg-1);
  25276. end;
  25277. end;
  25278. { TSQLTableJSON }
  25279. function TSQLTableJSON.PrivateCopyChanged(aJSON: PUTF8Char; aLen: integer): boolean;
  25280. var Hash: cardinal;
  25281. begin
  25282. Hash := crc32c(0,pointer(aJSON),aLen);
  25283. result := (fPrivateCopyHash=0) or (Hash=0) or (Hash<>fPrivateCopyHash);
  25284. if not result then
  25285. exit;
  25286. SetString(fPrivateCopy,PAnsiChar(aJSON),aLen);
  25287. fPrivateCopyHash := Hash;
  25288. end;
  25289. function TSQLTableJSON.ParseAndConvert(Buffer: PUTF8Char; BufferLen: integer): boolean;
  25290. function GetFieldCountExpanded(P: PUTF8Char): integer;
  25291. var EndOfObject: AnsiChar;
  25292. begin
  25293. result := 0;
  25294. repeat
  25295. P := GotoNextJSONItem(P,2,@EndOfObject); // ignore Name+Value items
  25296. if P=nil then begin // unexpected end
  25297. result := 0;
  25298. exit;
  25299. end;
  25300. inc(result);
  25301. if EndOfObject='}' then break; // end of object
  25302. until false;
  25303. end;
  25304. var i, max, nfield, nrow, resmax, f: integer;
  25305. EndOfObject: AnsiChar;
  25306. P: PUTF8Char;
  25307. wasString: Boolean;
  25308. begin
  25309. result := false; // error on parsing
  25310. fFieldIndexID := -1;
  25311. if (self=nil) or (Buffer=nil) then
  25312. exit;
  25313. // go to start of object
  25314. P := GotoNextNotSpace(Buffer);
  25315. if IsNotExpandedBuffer(P,Buffer+BufferLen,fFieldCount,fRowCount) then begin
  25316. // A. Not Expanded format
  25317. (* {"fieldCount":9,"values":["ID","Int","Test","Unicode","Ansi","ValFloat","ValWord",
  25318. "ValDate","Next",0,0,"abcde+¬ef+á+¬","abcde+¬ef+á+¬","abcde+¬ef+á+¬",
  25319. 3.14159265300000E+0000,1203,"2009-03-10T21:19:36",0,..],"rowCount":20} *)
  25320. // 1. check RowCount and DataLen
  25321. if fRowCount<0 then begin // IsNotExpanded() notified wrong input
  25322. fRowCount := 0; // may occur if P^ content was invalid
  25323. exit;
  25324. end;
  25325. // 2. initialize and fill fResults[] PPUTF8CharArray memory
  25326. max := (fRowCount+1)*FieldCount;
  25327. SetLength(fJSONResults,max);
  25328. fResults := @fJSONResults[0];
  25329. // unescape+zeroify JSONData + fill fResults[] to proper place
  25330. dec(max);
  25331. f := 0;
  25332. for i := 0 to max do begin
  25333. // get a field
  25334. fJSONResults[i] := GetJSONFieldOrObjectOrArray(P,@wasString,nil,true);
  25335. if (P=nil) and (i<>max) then
  25336. exit; // failure (GetRowCountNotExpanded should have detected it)
  25337. if i>=FieldCount then begin
  25338. if wasString then
  25339. Include(fFieldParsedAsString,f); // mark column was "string"
  25340. inc(f);
  25341. if f=FieldCount then
  25342. f := 0; // check all rows
  25343. end;
  25344. end;
  25345. end else begin
  25346. // B. Expanded format
  25347. (* [{"ID":0,"Int":0,"Test":"abcde+¬ef+á+¬","Unicode":"abcde+¬ef+á+¬","Ansi":
  25348. "abcde+¬ef+á+¬","ValFloat": 3.14159265300000E+0000,"ValWord":1203,
  25349. "ValDate":"2009-03-10T21:19:36","Next":0},{..}] *)
  25350. // 1. get fields count from first row
  25351. while P^<>'[' do if P^=#0 then exit else inc(P); // need an array of objects
  25352. repeat inc(P); if P^=#0 then exit; until P^ in ['{',']']; // go to object beginning
  25353. if P^=']' then begin // [] -> void data
  25354. result := true;
  25355. exit;
  25356. end;
  25357. inc(P);
  25358. nfield := GetFieldCountExpanded(P);
  25359. if nField=0 then
  25360. exit; // invalid data for first row
  25361. // 2. get values (assume fields are always the same as in the first object)
  25362. max := nfield; // index to start storing values in fResults[]
  25363. resmax := nfield*2;
  25364. SetLength(fJSONResults,resmax); // space for field names + 1 data row
  25365. nrow := 0;
  25366. repeat // let fJSONResults[] point to unescaped+zeroified JSON values
  25367. f := 0;
  25368. for i := 0 to nfield-1 do begin
  25369. if nrow=0 then // get field name from 1st Row
  25370. fJSONResults[i] := GetJSONPropName(P) else
  25371. P := GotoNextJSONItem(P); // ignore field name for later rows
  25372. if max>=resmax then begin // check space inside loop for GPF security
  25373. inc(resmax,resmax shr 3+nfield shl 8);
  25374. SetLength(fJSONResults,resmax); // enough space for 256 more rows
  25375. end;
  25376. if P=nil then break; // normal end: no more field name
  25377. fJSONResults[max] := GetJSONFieldOrObjectOrArray(
  25378. P,@wasString,@EndOfObject,true);
  25379. if P=nil then begin
  25380. nfield := 0;
  25381. break; // unexpected end
  25382. end;
  25383. if wasString then // mark column was "string"
  25384. Include(fFieldParsedAsString,f);
  25385. inc(f);
  25386. inc(max);
  25387. if f=nField then
  25388. f := 0; // check all rows
  25389. end;
  25390. if P=nil then
  25391. break; // unexpected end
  25392. if EndOfObject<>'}' then
  25393. break; // data field layout is not consistent: should never happen
  25394. inc(nrow);
  25395. while (P^<>'{') and (P^<>']') do // go to next object beginning
  25396. if P^=#0 then
  25397. exit else
  25398. inc(P);
  25399. if P^=']' then
  25400. break else
  25401. inc(P); // jmp '{'
  25402. until false;
  25403. if max<>(nrow+1)*nfield then begin // field count must be the same for all objects
  25404. fFieldCount := 0;
  25405. fRowCount := 0;
  25406. exit; // data field layout is not consistent: should never happen
  25407. end;
  25408. // 3. save field pointers to fResults[]
  25409. SetLength(fJSONResults,max); // resize to exact size
  25410. fResults := @fJSONResults[0];
  25411. fFieldCount := nfield;
  25412. fRowCount := nrow;
  25413. end;
  25414. for i := 0 to fFieldCount-1 do
  25415. if IsRowID(fResults[i]) then begin
  25416. fFieldIndexID := i;
  25417. break;
  25418. end;
  25419. result := true; // if we reached here, means successfull conversion from P^
  25420. end;
  25421. function TSQLTableJSON.UpdateFrom(const aJSON: RawUTF8; var Refreshed: boolean;
  25422. PCurrentRow: PInteger): boolean;
  25423. var len: Integer;
  25424. begin
  25425. len := length(aJSON);
  25426. if PrivateCopyChanged(pointer(aJSON),len) then
  25427. if ParseAndConvert(pointer(fPrivateCopy),len) then begin
  25428. // parse success from new aJSON data -> need some other update?
  25429. if Assigned(fIDColumn) then begin
  25430. // ID column was hidden -> do it again
  25431. Finalize(fIDColumn);
  25432. IDColumnHide;
  25433. end;
  25434. with fSortParams do
  25435. if FieldCount<>0 then
  25436. // TSQLTable.SortFields() was called -> do it again
  25437. SortFields(FieldIndex,Asc,PCurrentRow,FieldType);
  25438. Refreshed := true;
  25439. result := true;
  25440. end else
  25441. // parse error
  25442. result := false else
  25443. // data didn't change (fPrivateCopyHash checked)
  25444. result := true;
  25445. end;
  25446. constructor TSQLTableJSON.Create(const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer);
  25447. begin // don't raise exception on error parsing
  25448. inherited Create(aSQL);
  25449. ParseAndConvert(JSONBuffer,JSONBufferLen);
  25450. end;
  25451. constructor TSQLTableJSON.Create(const aSQL, aJSON: RawUTF8);
  25452. var len: integer;
  25453. begin
  25454. len := length(aJSON);
  25455. SetString(fPrivateCopy,PAnsiChar(pointer(aJSON)),len);
  25456. Create(aSQL,pointer(fPrivateCopy),len);
  25457. end;
  25458. constructor TSQLTableJSON.CreateFromTables(const Tables: array of TSQLRecordClass;
  25459. const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer);
  25460. begin // don't raise exception on error parsing
  25461. inherited CreateFromTables(Tables,aSQL);
  25462. ParseAndConvert(JSONBuffer,JSONBufferLen);
  25463. end;
  25464. constructor TSQLTableJSON.CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL,
  25465. aJSON: RawUTF8);
  25466. var len: integer;
  25467. begin
  25468. len := length(aJSON);
  25469. SetString(fPrivateCopy,PAnsiChar(pointer(aJSON)),len);
  25470. CreateFromTables(Tables,aSQL,pointer(fPrivateCopy),len);
  25471. end;
  25472. constructor TSQLTableJSON.CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType;
  25473. const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer);
  25474. begin // don't raise exception on error parsing
  25475. inherited CreateWithColumnTypes(ColumnTypes,aSQL);
  25476. ParseAndConvert(JSONBuffer,JSONBufferLen);
  25477. end;
  25478. constructor TSQLTableJSON.CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType;
  25479. const aSQL, aJSON: RawUTF8);
  25480. var len: integer;
  25481. begin
  25482. len := length(aJSON);
  25483. SetString(fPrivateCopy,PAnsiChar(pointer(aJSON)),len);
  25484. CreateWithColumnTypes(ColumnTypes,aSQL,pointer(fPrivateCopy),len);
  25485. end;
  25486. { TINIWriter }
  25487. procedure TINIWriter.WriteObject(Value: TObject; const SubCompName: RawUTF8='';
  25488. WithSection: boolean=true);
  25489. var P: PPropInfo;
  25490. i, V: integer;
  25491. VT: shortstring; // for str()
  25492. Obj: TObject;
  25493. tmp: RawUTF8;
  25494. {$ifndef NOVARIANTS}
  25495. VV: Variant;
  25496. {$endif}
  25497. begin
  25498. if Value<>nil then begin
  25499. if WithSection then
  25500. // new TObject.ClassName is UnicodeString (Delphi 20009) -> inline code with
  25501. // vmtClassName = UTF-8 encoded text stored in a shortstring = -44
  25502. Add(#13'[%]'#13,[PShortString(PPointer(PPtrInt(Value)^+vmtClassName)^)^]);
  25503. for i := 1 to InternalClassPropInfo(Value.ClassType,P) do begin
  25504. case P^.PropType^.Kind of
  25505. tkInt64{$ifdef FPC}, tkQWord{$endif}:
  25506. Add('%%=%'#13,[SubCompName,P^.Name,P^.GetInt64Prop(Value)]);
  25507. {$ifdef FPC}tkBool,{$endif}
  25508. tkEnumeration, tkInteger, tkSet: begin
  25509. V := P^.GetOrdProp(Value);
  25510. if V<>P^.Default then
  25511. Add('%%=%'#13,[SubCompName,P^.Name,V]);
  25512. end;
  25513. {$ifdef FPC}tkAString,{$endif} tkLString, tkWString
  25514. {$ifdef HASVARUSTRING},tkUString{$endif}: begin
  25515. P^.GetLongStrValue(Value,tmp);
  25516. Add('%%=%'#13,[SubCompName,P^.Name,tmp]);
  25517. end;
  25518. tkFloat: begin
  25519. VT[0] := AnsiChar(ExtendedToString(VT,P^.GetFloatProp(Value),DOUBLE_PRECISION));
  25520. Add('%%=%'#13,[SubCompName,P^.Name,VT]);
  25521. end;
  25522. tkDynArray: begin
  25523. Add('%%=%'#13,[SubCompName,P^.Name]);
  25524. AddDynArrayJSON(P^.GetDynArray(Value));
  25525. Add(#13);
  25526. end;
  25527. {$ifdef PUBLISHRECORD}
  25528. tkRecord{$ifdef FPC},tkObject{$endif}:
  25529. Add('%%=%'#13,[SubCompName,P^.Name,BinToBase64WithMagic(
  25530. RecordSave(P^.GetFieldAddr(Value)^,P^.PropType^))]);
  25531. {$endif}
  25532. tkClass: begin
  25533. Obj := P^.GetObjProp(Value);
  25534. if (Obj<>nil) and ClassHasPublishedFields(PPointer(Obj)^) then
  25535. WriteObject(Obj,SubCompName+ToUTF8(P^.Name)+'.',false);
  25536. end;
  25537. {$ifndef NOVARIANTS}
  25538. tkVariant: begin // stored as JSON, e.g. '1.234' or '"text"'
  25539. P^.GetVariantProp(Value,VV);
  25540. Add('%%=%'#13,[SubCompName,P^.Name,VariantSaveJSON(VV)]);
  25541. end;
  25542. {$endif}
  25543. end; // tkString (shortstring) and tkInterface is not handled
  25544. P := P^.Next;
  25545. end;
  25546. end;
  25547. end;
  25548. function UTF8ContentNumberType(P: PUTF8Char): TSQLFieldType;
  25549. begin
  25550. if (P=nil) or ((PInteger(P)^=ord('n')+ord('u')shl 8+ord('l')shl 16+
  25551. ord('l')shl 24) and (P[4]=#0)) then
  25552. result := sftUnknown else
  25553. case TextToVariantNumberType(P) of
  25554. varInt64: result := sftInteger;
  25555. varDouble: result := sftFloat;
  25556. varCurrency: result := sftCurrency;
  25557. else result := sftUTF8Text;
  25558. end;
  25559. end;
  25560. function UTF8ContentType(P: PUTF8Char): TSQLFieldType;
  25561. var c,len: integer;
  25562. begin
  25563. if P<>nil then begin
  25564. if P^ in [#1..' '] then repeat inc(P) until not(P^ in [#1..' ']);
  25565. if (PInteger(P)^=NULL_LOW) and (P[4]=#0) then
  25566. result := sftUnknown else
  25567. // don't check for 'false' or 'true' here, since their UTF-8 value is 0/1
  25568. if P^ in ['-','0'..'9'] then
  25569. case TextToVariantNumberType(P) of
  25570. varInt64: result := sftInteger;
  25571. varDouble: result := sftFloat;
  25572. varCurrency: result := sftCurrency;
  25573. else begin
  25574. len := StrLen(P);
  25575. if (len>15) and (Iso8601ToTimeLogPUTF8Char(P,len)<>0) then
  25576. result := sftDateTime else
  25577. result := sftUTF8Text;
  25578. end;
  25579. end else begin
  25580. c := PInteger(P)^ and $00ffffff;
  25581. if (c=JSON_BASE64_MAGIC) or ((P^='''') and isBlobHex(P)) then
  25582. result := sftBlob else
  25583. if c=JSON_SQLDATE_MAGIC then
  25584. result := sftDateTime else
  25585. result := sftUTF8Text;
  25586. end;
  25587. end else
  25588. result := sftUnknown;
  25589. end;
  25590. { TPropInfo }
  25591. function TPropInfo.ClassFromJSON(Instance: TObject; From: PUTF8Char;
  25592. var Valid: boolean; Options: TJSONToObjectOptions): PUTF8Char;
  25593. var Field: ^TObject;
  25594. tmp: TObject;
  25595. begin
  25596. valid := false;
  25597. result := nil;
  25598. if (@self=nil) or (PropType^.Kind<>tkClass) or (Instance=nil) then
  25599. exit;
  25600. if SetterIsField then
  25601. // setter to field -> direct in-memory access
  25602. Field := SetterAddr(Instance) else
  25603. {$ifndef FPC}
  25604. if SetProc<>0 then begin
  25605. // it is a setter method -> create a temporary object
  25606. tmp := PropType^.ClassCreate;
  25607. try
  25608. result := JSONToObject(tmp,From,Valid,nil,Options);
  25609. if not Valid then
  25610. FreeAndNil(tmp) else begin
  25611. SetOrdProp(Instance,PtrInt(tmp)); // PtrInt(tmp) is OK for CPU64
  25612. if j2oSetterExpectsToFreeTempInstance in Options then
  25613. FreeAndNil(tmp);
  25614. end;
  25615. except
  25616. on Exception do
  25617. tmp.Free;
  25618. end;
  25619. exit;
  25620. end else
  25621. {$endif}
  25622. if GetterIsField then
  25623. // no setter -> use direct in-memory access from getter (if available)
  25624. Field := GetterAddr(Instance) else
  25625. // no setter, nor direct field offset -> impossible to set the instance
  25626. exit;
  25627. result := JSONToObject(Field^,From,Valid,nil,Options);
  25628. end;
  25629. function TPropInfo.GetOrdValue(Instance: TObject): PtrInt;
  25630. begin
  25631. if (Instance<>nil) and (@self<>nil) and
  25632. (PropType^.Kind in [
  25633. tkInteger,tkEnumeration,tkSet,{$ifdef FPC}tkBool,{$endif}tkClass]) then
  25634. result := GetOrdProp(Instance) else
  25635. result := -1;
  25636. end;
  25637. function TPropInfo.GetInt64Value(Instance: TObject): Int64;
  25638. begin
  25639. if (Instance<>nil) and (@self<>nil) then
  25640. case PropType^.Kind of
  25641. tkInteger,tkEnumeration,tkSet,tkChar,tkWChar,tkClass{$ifdef FPC},tkBool{$endif}:
  25642. result := GetOrdProp(Instance);
  25643. tkInt64{$ifdef FPC},tkQWord{$endif}:
  25644. result := GetInt64Prop(Instance);
  25645. else result := 0;
  25646. end else
  25647. result := 0;
  25648. end;
  25649. function TPropInfo.GetCurrencyValue(Instance: TObject): Currency;
  25650. begin
  25651. if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkFloat) and
  25652. (PropType^.FloatType=ftCurr) then
  25653. result := GetCurrencyProp(Instance) else
  25654. result := 0;
  25655. end;
  25656. function TPropInfo.GetExtendedValue(Instance: TObject): TSynExtended;
  25657. begin
  25658. if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkFloat) then
  25659. result := GetFloatProp(Instance) else
  25660. result := 0;
  25661. end;
  25662. procedure TPropInfo.SetExtendedValue(Instance: TObject; const Value: TSynExtended);
  25663. begin
  25664. if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkFloat) then
  25665. SetFloatProp(Instance,Value);
  25666. end;
  25667. function TPropInfo.GetDynArray(Instance: TObject): TDynArray;
  25668. begin
  25669. result.Init(TypeInfo,GetFieldAddr(Instance)^);
  25670. end;
  25671. procedure TPropInfo.GetDynArray(Instance: TObject; var result: TDynArray);
  25672. begin
  25673. result.Init(TypeInfo,GetFieldAddr(Instance)^);
  25674. end;
  25675. function TPropInfo.DynArrayIsObjArray: boolean;
  25676. begin
  25677. if PropType^.Kind=tkDynArray then
  25678. result := ObjArraySerializers.Find(TypeInfo)<>nil else
  25679. result := false;
  25680. end;
  25681. function TPropInfo.DynArrayIsObjArrayInstance: PClassInstance;
  25682. begin
  25683. if PropType^.Kind<>tkDynArray then
  25684. result := nil else
  25685. result := TJSONSerializer.RegisterObjArrayFindType(TypeInfo);
  25686. end;
  25687. procedure TPropInfo.GetLongStrValue(Instance: TObject; var result: RawUTF8);
  25688. var tmp: RawByteString;
  25689. tmpWS: WideString;
  25690. cp: integer;
  25691. begin
  25692. if (Instance<>nil) and (@self<>nil) then
  25693. case PropType^.Kind of
  25694. {$ifdef FPC}tkAString,{$endif} tkLString: begin
  25695. GetLongStrProp(Instance,tmp);
  25696. if tmp='' then
  25697. result := '' else begin
  25698. cp := PropType^.AnsiStringCodePage;
  25699. case cp of
  25700. CP_UTF8: result := tmp;
  25701. CP_SQLRAWBLOB: result := TSQLRawBlobToBlob(TSQLRawBlob(tmp));
  25702. else result := TSynAnsiConvert.Engine(cp).AnsiToUTF8(tmp);
  25703. end;
  25704. end;
  25705. end;
  25706. {$ifdef HASVARUSTRING}
  25707. tkUString:
  25708. result := UnicodeStringToUTF8(GetUnicodeStrProp(Instance));
  25709. {$endif}
  25710. tkWString: begin
  25711. GetWideStrProp(Instance,tmpWS);
  25712. RawUnicodeToUtf8(pointer(tmpWS),length(tmpWS),result);
  25713. end;
  25714. else result := '';
  25715. end
  25716. else result := '';
  25717. end;
  25718. procedure TPropInfo.GetRawByteStringValue(Instance: TObject; var Value: RawByteString);
  25719. begin
  25720. if (Instance<>nil) and (@self<>nil) and
  25721. (PropType^.Kind in [{$ifdef FPC}tkAString,{$endif}tkLString]) then
  25722. GetLongStrProp(Instance,Value) else
  25723. Value := '';
  25724. end;
  25725. procedure TPropInfo.SetLongStrValue(Instance: TObject; const Value: RawUTF8);
  25726. procedure HandleAnsiString(Instance: TObject; const Value: RawUTF8; cp: integer);
  25727. var tmp: RawByteString;
  25728. begin
  25729. if cp=CP_SQLRAWBLOB then
  25730. tmp := BlobToTSQLRawBlob(Value) else
  25731. tmp := TSynAnsiConvert.Engine(cp).UTF8ToAnsi(Value);
  25732. SetLongStrProp(Instance,tmp);
  25733. end;
  25734. {$ifdef HASVARUSTRING}
  25735. procedure HandleUnicode(Instance: TObject; const Value: RawUTF8);
  25736. begin
  25737. SetUnicodeStrProp(Instance,UTF8DecodeToUnicodeString(Value));
  25738. end;
  25739. {$endif}
  25740. procedure HandleWideString(Instance: TObject; const Value: RawUTF8);
  25741. begin
  25742. SetWideStrProp(Instance,UTF8ToWideString(Value));
  25743. end;
  25744. var cp: integer;
  25745. begin
  25746. if (Instance<>nil) and (@self<>nil) then
  25747. case PropType^.Kind of
  25748. {$ifdef FPC}tkAString,{$endif}tkLString: begin
  25749. if Value<>'' then begin
  25750. cp := PropType^.AnsiStringCodePage;
  25751. if cp=CP_UTF8 then
  25752. SetLongStrProp(Instance,Value) else
  25753. HandleAnsiString(Instance,Value,cp);
  25754. end else
  25755. SetLongStrProp(Instance,'');
  25756. end;
  25757. {$ifdef HASVARUSTRING}
  25758. tkUString:
  25759. HandleUnicode(Instance,Value);
  25760. {$endif}
  25761. tkWString:
  25762. HandleWideString(Instance,Value);
  25763. end;
  25764. end;
  25765. const null_vardata: TVarData = (VType: varNull);
  25766. {$ifndef NOVARIANTS}
  25767. procedure TPropInfo.SetFromVariant(Instance: TObject; const Value: variant);
  25768. var i: integer;
  25769. i64: Int64;
  25770. u: RawUTF8;
  25771. d: double;
  25772. begin
  25773. if (Instance<>nil) and (@self<>nil) then
  25774. case PropType^.Kind of
  25775. tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}:
  25776. if VariantToInteger(Value,i) then
  25777. SetOrdProp(Instance,i) else
  25778. if (PropType^.Kind=tkEnumeration) and VariantToUTF8(Value,u) then begin
  25779. i := PropType^.EnumBaseType^.GetEnumNameValue(pointer(u),length(u));
  25780. if i>=0 then
  25781. SetOrdProp(Instance,i)
  25782. end;
  25783. tkInt64{$ifdef FPC},tkQWord{$endif}:
  25784. if VariantToInt64(Value,i64) then
  25785. SetInt64Prop(Instance,i64);
  25786. {$ifdef HASVARUSTRING}tkUString,{$endif}
  25787. tkLString, tkWString {$ifdef FPC},tkAString{$endif}:
  25788. if VariantToUTF8(Value,u) then
  25789. SetLongStrValue(Instance,u);
  25790. tkFloat:
  25791. if VariantToDouble(Value,d) then
  25792. SetFloatProp(Instance,d);
  25793. tkVariant:
  25794. SetVariantProp(Instance,Value);
  25795. tkClass:
  25796. DocVariantToObject(_Safe(Value)^,GetObjProp(Instance));
  25797. tkDynArray:
  25798. DocVariantToObjArray(_Safe(Value)^,GetFieldAddr(Instance)^,
  25799. TJSONSerializer.RegisterObjArrayFindType(TypeInfo));
  25800. {$ifdef PUBLISHRECORD}
  25801. tkRecord{$ifdef FPC},tkObject{$endif}: begin
  25802. VariantSaveJSON(Value,twJSONEscape,u);
  25803. RecordLoadJSON(GetFieldAddr(Instance)^,pointer(u),TypeInfo);
  25804. end;
  25805. {$endif}
  25806. end;
  25807. end;
  25808. {$endif NOVARIANTS}
  25809. procedure TPropInfo.SetDefaultValue(Instance: TObject; FreeAndNilNestedObjects: boolean);
  25810. var Item: TObject;
  25811. da: TDynArray;
  25812. {$ifdef PUBLISHRECORD}
  25813. addr: pointer;
  25814. {$endif}
  25815. begin
  25816. if (Instance<>nil) and (@self<>nil) then
  25817. case PropType^.Kind of
  25818. tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}:
  25819. SetOrdProp(Instance,0);
  25820. tkInt64{$ifdef FPC},tkQWord{$endif}:
  25821. SetInt64Prop(Instance,0);
  25822. tkLString{$ifdef FPC},tkAString{$endif}:
  25823. SetLongStrProp(Instance,'');
  25824. {$ifdef HASVARUSTRING}
  25825. tkUString:
  25826. SetUnicodeStrProp(Instance,'');
  25827. {$endif}
  25828. tkWString:
  25829. SetWideStrProp(Instance,'');
  25830. tkFloat:
  25831. SetFloatProp(Instance,0);
  25832. {$ifndef NOVARIANTS}
  25833. tkVariant:
  25834. SetVariantProp(Instance,variant(null_vardata));
  25835. {$endif}
  25836. tkClass:
  25837. begin
  25838. Item := GetObjProp(Instance);
  25839. if Item<>nil then
  25840. if FreeAndNilNestedObjects then begin
  25841. SetOrdProp(Instance,0); // mimic FreeAndNil()
  25842. Item.Free;
  25843. end else
  25844. ClearObject(Item,false);
  25845. end;
  25846. tkDynArray: begin
  25847. GetDynArray(Instance,da);
  25848. da.Count := 0; // will handle also any T*ObjArray
  25849. end;
  25850. {$ifdef PUBLISHRECORD}
  25851. tkRecord{$ifdef FPC},tkObject{$endif}: begin
  25852. addr := GetFieldAddr(Instance);
  25853. RecordClear(addr^,TypeInfo);
  25854. FillcharFast(addr^,TypeInfo^.RecordType^.Size,0);
  25855. end;
  25856. {$endif}
  25857. end;
  25858. end;
  25859. function TPropInfo.GetGenericStringValue(Instance: TObject): string;
  25860. var tmp: RawUTF8;
  25861. begin
  25862. if (Instance=nil) or (@self=nil) then
  25863. result := '' else
  25864. case PropType^.Kind of
  25865. {$ifdef FPC}tkAString,{$endif} tkLString, tkWString: begin
  25866. GetLongStrValue(Instance,tmp);
  25867. result := UTF8ToString(tmp);
  25868. end;
  25869. {$ifdef HASVARUSTRING}
  25870. tkUString:
  25871. result := string(GetUnicodeStrProp(Instance));
  25872. {$endif}else result := '';
  25873. end;
  25874. end;
  25875. procedure TPropInfo.SetGenericStringValue(Instance: TObject; const Value: string);
  25876. begin
  25877. if (Instance<>nil) and (@self<>nil) then
  25878. case PropType^.Kind of
  25879. {$ifdef FPC}tkAString,{$endif}tkLString, tkWString:
  25880. SetLongStrValue(Instance,StringToUtf8(Value));
  25881. {$ifdef HASVARUSTRING}
  25882. tkUString:
  25883. SetUnicodeStrProp(Instance,UnicodeString(Value));
  25884. {$endif}
  25885. end;
  25886. end;
  25887. {$ifdef HASVARUSTRING}
  25888. function TPropInfo.GetUnicodeStrValue(Instance: TObject): UnicodeString;
  25889. begin
  25890. if (Instance<>nil) and (@self<>nil) and
  25891. (PropType^.Kind=tkUString) then
  25892. result := GetUnicodeStrProp(Instance);
  25893. end;
  25894. procedure TPropInfo.SetUnicodeStrValue(Instance: TObject; const Value: UnicodeString);
  25895. begin
  25896. if (Instance<>nil) and (@self<>nil) and
  25897. (PropType^.Kind=tkUString) then
  25898. SetUnicodeStrProp(Instance,Value);
  25899. end;
  25900. {$endif HASVARUSTRING}
  25901. procedure TPropInfo.SetOrdValue(Instance: TObject; Value: PtrInt);
  25902. begin
  25903. if (Instance<>nil) and (@self<>nil) and
  25904. (PropType^.Kind in [
  25905. tkInteger,tkEnumeration,tkSet,{$ifdef FPC}tkBool,{$endif}tkClass]) then
  25906. SetOrdProp(Instance,Value);
  25907. end;
  25908. procedure TPropInfo.SetInt64Value(Instance: TObject; Value: Int64);
  25909. begin
  25910. if (Instance<>nil) and (@self<>nil) then
  25911. case PropType^.Kind of
  25912. tkInteger,tkEnumeration,tkSet,tkChar,tkWChar,tkClass{$ifdef FPC},tkBool{$endif}:
  25913. SetOrdProp(Instance,Value);
  25914. tkInt64{$ifdef FPC}, tkQWord{$endif}:
  25915. SetInt64Prop(Instance,Value);
  25916. end;
  25917. end;
  25918. function TPropInfo.SameValue(Source: TObject; DestInfo: PPropInfo; Dest: TObject): boolean;
  25919. {$ifndef NOVARIANTS}
  25920. function CompareVariants: boolean;
  25921. var VS,VD: Variant;
  25922. begin
  25923. GetVariantProp(Source,VS);
  25924. DestInfo^.GetVariantProp(Dest,VD);
  25925. result := VS=VD; // rely on Variants.pas comparison
  25926. end;
  25927. {$endif}
  25928. function CompareStrings: Boolean;
  25929. var US,UD: RawUTF8;
  25930. begin
  25931. GetLongStrValue(Source,US);
  25932. DestInfo^.GetLongStrValue(Dest,UD);
  25933. result := US=UD;
  25934. end;
  25935. var kS,kD: TTypeKind;
  25936. daS,daD: TDynArray;
  25937. i: integer;
  25938. begin
  25939. if Source=Dest then begin
  25940. result := true;
  25941. exit;
  25942. end;
  25943. result := false;
  25944. if (Source=nil) or (Dest=nil) or (@self=nil) or (DestInfo=nil) then
  25945. exit;
  25946. kS := PropType^.Kind;
  25947. kD := DestInfo^.PropType^.Kind;
  25948. if kS in tkStringTypes then
  25949. if kD in tkStringTypes then
  25950. result := CompareStrings else
  25951. exit else
  25952. if kS in tkOrdinalTypes then
  25953. if kD in tkOrdinalTypes then
  25954. result := GetInt64Value(Source)=DestInfo^.GetInt64Value(Dest) else
  25955. exit else
  25956. if kS=kD then
  25957. case KS of
  25958. tkClass:
  25959. result := ObjectEquals(GetObjProp(Source),DestInfo^.GetObjProp(Dest));
  25960. tkFloat: begin
  25961. if DestInfo^.PropType^.FloatType=PropType^.FloatType then
  25962. case PropType^.FloatType of
  25963. ftCurr: begin
  25964. if GetterIsField and ((@self=DestInfo) or DestInfo^.GetterIsField) then
  25965. result := GetInt64Value(Source)=DestInfo^.GetInt64Value(Dest) else
  25966. result := GetCurrencyProp(Source)=DestInfo^.GetCurrencyProp(Dest);
  25967. exit;
  25968. end;
  25969. ftDoub: begin
  25970. if GetterIsField and ((@self=DestInfo) or DestInfo^.GetterIsField) then
  25971. result := GetInt64Value(Source)=DestInfo^.GetInt64Value(Dest) else
  25972. result := SynCommons.SameValue(GetDoubleProp(Source),DestInfo^.GetDoubleProp(Dest));
  25973. exit;
  25974. end;
  25975. end;
  25976. result := SynCommons.SameValueFloat(GetFloatProp(Source),DestInfo^.GetFloatProp(Dest));
  25977. end;
  25978. tkDynArray: begin
  25979. GetDynArray(Source,daS);
  25980. DestInfo^.GetDynArray(Dest,daD);
  25981. if daS.Count=daD.Count then
  25982. if DynArrayIsObjArray and
  25983. ((@self=DestInfo) or DestInfo^.DynArrayIsObjArray) then begin
  25984. for i := 0 to daS.Count-1 do
  25985. if not ObjectEquals(PObjectArray(daS.Value)[i],PObjectArray(daD.Value)[i]) then
  25986. exit;
  25987. result := true;
  25988. end else
  25989. result := daD.Equals(daS);
  25990. end;
  25991. {$ifndef NOVARIANTS}
  25992. tkVariant:
  25993. result := CompareVariants;
  25994. {$endif}
  25995. end;
  25996. end;
  25997. function ClassFieldPropInstanceMatchingClass(
  25998. aSearchedInstance: TObject; aSearchedClassType: TClass): TObject;
  25999. var P: PPropInfo;
  26000. begin
  26001. result := aSearchedInstance;
  26002. if (aSearchedInstance=nil) or
  26003. aSearchedInstance.InheritsFrom(aSearchedClassType) then
  26004. exit;
  26005. P := ClassFieldPropWithParentsFromClassType(PPointer(aSearchedInstance)^,aSearchedClassType);
  26006. if P<>nil then begin
  26007. result := P^.GetObjProp(aSearchedInstance);
  26008. if result=nil then
  26009. result := aSearchedInstance;
  26010. end;
  26011. end;
  26012. function TPropInfo.CopyToNewObject(aFrom: TObject): TObject;
  26013. var aClass: TClass;
  26014. aInstance: TClassInstance;
  26015. begin
  26016. if aFrom=nil then begin
  26017. result := nil;
  26018. exit;
  26019. end;
  26020. aClass := PropType^.ClassType^.ClassType;
  26021. aInstance.Init(aClass);
  26022. result := aInstance.CreateNew;
  26023. try
  26024. CopyObject(ClassFieldPropInstanceMatchingClass(aFrom,aClass),result);
  26025. except
  26026. FreeAndNil(result); // avoid memory leak if error during new instance copy
  26027. end;
  26028. end;
  26029. procedure TPropInfo.CopyValue(Source, Dest: TObject; DestInfo: PPropInfo);
  26030. var Value: RawByteString;
  26031. WS: WideString;
  26032. {$ifndef NOVARIANTS}
  26033. V: variant;
  26034. {$endif}
  26035. S,D: TObject;
  26036. kS,kD: TTypeKind;
  26037. ft: TSQLFieldType;
  26038. label i64, int, dst, obj, str;
  26039. begin
  26040. if DestInfo=nil then
  26041. DestInfo := @self;
  26042. if (@self=nil) or (Source=nil) or (Dest=Source) or (Dest=nil) then
  26043. exit;
  26044. kS := PropType^.Kind;
  26045. kD := DestInfo^.PropType^.Kind;
  26046. case kS of
  26047. {$ifdef FPC}tkBool,{$endif}
  26048. tkEnumeration, tkInteger, tkSet, tkChar, tkWChar:
  26049. int: if DestInfo=@Self then
  26050. SetOrdProp(Dest,GetOrdProp(Source)) else
  26051. dst: if kD in tkOrdinalTypes then // use Int64 to handle e.g. cardinal
  26052. DestInfo^.SetInt64Value(Dest,GetInt64Value(Source));
  26053. tkClass: begin
  26054. ft := PropType^.ClassSQLFieldType;
  26055. case ft of
  26056. sftID: // TSQLRecord published properties (sftID)
  26057. if TSQLRecord(Source).fFill.JoinedFields then
  26058. // -> pre-allocated fields by Create*Joined()
  26059. goto obj else
  26060. // -> these are not class instances, but INTEGER reference to records
  26061. goto int;
  26062. sftMany, sftObject: begin
  26063. // generic case: copy also class content (create instances)
  26064. obj: S := GetObjProp(Source);
  26065. if (DestInfo=@self) or
  26066. ((kD=tkClass) and (DestInfo^.PropType^.ClassSQLFieldType=ft)) then begin
  26067. D := DestInfo.GetObjProp(Dest);
  26068. {$ifndef LVCL}
  26069. if S.InheritsFrom(TCollection) then
  26070. CopyCollection(TCollection(S),TCollection(D)) else
  26071. {$endif} begin
  26072. D.Free; // release previous D instance then set a new copy of S
  26073. DestInfo.SetOrdProp(Dest,PtrInt(DestInfo^.CopyToNewObject(S)));
  26074. end;
  26075. end;
  26076. end;
  26077. end;
  26078. end;
  26079. tkInt64{$ifdef FPC}, tkQWord{$endif}:
  26080. if DestInfo=@self then
  26081. // works also with TID, TTimeLog, Double and Currency
  26082. i64: SetInt64Prop(Dest,GetInt64Prop(Source)) else
  26083. goto dst;
  26084. tkFloat:
  26085. if DestInfo=@self then
  26086. if (PropType^.FloatType in [ftDoub,ftCurr]) and
  26087. GetterIsField and SetterIsField then
  26088. goto I64 else
  26089. SetFloatProp(Dest,GetFloatProp(Source)) else
  26090. if kD=tkFloat then
  26091. DestInfo.SetFloatProp(Dest,GetFloatProp(Source));
  26092. {$ifdef FPC}tkAString,{$endif}
  26093. tkLString:
  26094. if kD=tkLString then begin
  26095. GetLongStrProp(Source,Value);
  26096. DestInfo.SetLongStrProp(Dest,Value);
  26097. end else
  26098. str: if kD in tkStringTypes then begin
  26099. GetLongStrValue(Source,RawUTF8(Value));
  26100. DestInfo.SetLongStrValue(Dest,RawUTF8(Value));
  26101. end;
  26102. {$ifdef HASVARUSTRING}
  26103. tkUString:
  26104. if kD=tkUString then
  26105. DestInfo.SetUnicodeStrProp(Dest,GetUnicodeStrProp(Source)) else
  26106. goto str;
  26107. {$endif}
  26108. tkWString:
  26109. if kD=tkWString then begin
  26110. GetWideStrProp(Source,WS);
  26111. DestInfo.SetWideStrProp(Dest,WS);
  26112. end else
  26113. goto str;
  26114. tkDynArray:
  26115. if (DestInfo=@self) or (TypeInfo=DestInfo.TypeInfo) then
  26116. DestInfo.GetDynArray(Dest).Copy(GetDynArray(Source));
  26117. tkRecord{$ifdef FPC},tkObject{$endif}:
  26118. if (DestInfo=@self) or (TypeInfo=DestInfo.TypeInfo) then
  26119. RecordCopy(DestInfo.GetFieldAddr(Dest)^,GetFieldAddr(Source)^,TypeInfo);
  26120. {$ifndef NOVARIANTS}
  26121. tkVariant:
  26122. if kD=tkVariant then begin
  26123. GetVariantProp(Source,V);
  26124. DestInfo.SetVariantProp(Dest,V);
  26125. end;
  26126. {$endif}
  26127. end; // note: tkString (shortstring) and tkInterface not handled
  26128. end;
  26129. function TPropInfo.GetFieldAddr(Instance: TObject): pointer;
  26130. begin
  26131. if not GetterIsField then
  26132. if not SetterIsField then
  26133. // both are methods -> returns nil
  26134. result := nil else
  26135. // field - Setter is the field offset in the instance data
  26136. result := SetterAddr(Instance) else
  26137. // field - Getter is the field offset in the instance data
  26138. result := GetterAddr(Instance);
  26139. end;
  26140. function TPropInfo.IsBlob: boolean;
  26141. begin
  26142. result := (@self<>nil) and (TypeInfo=system.TypeInfo(TSQLRawBlob));
  26143. end;
  26144. function TPropInfo.IsStored(Instance: TObject): boolean;
  26145. type // function(Instance: TObject) trick does not work with CPU64 :(
  26146. TStoredProc = function: Boolean of object;
  26147. var Call: TMethod;
  26148. begin
  26149. {$ifdef FPC} // extracted from IsStoredProp() function in typinfo.pp
  26150. result := ((PropProcs shr 4) and 3=ptconst) and LongBool(StoredProc);
  26151. {$else} // Delphi version
  26152. if (StoredProc and (not PtrInt($ff)))=0 then
  26153. result := boolean(StoredProc) else
  26154. if Instance=nil then
  26155. // field or method without Instance specified -> assume "stored true"
  26156. result := true else
  26157. if PropWrap(StoredProc).Kind=$ff then
  26158. result := PBoolean(PtrInt(Instance)+StoredProc and $00FFFFFF)^ else begin
  26159. if PropWrap(StoredProc).Kind=$fe then
  26160. Call.Code := pointer((PPtrUInt(PPtrInt(Instance)^+SmallInt(StoredProc))^)) else
  26161. Call.Code := pointer(StoredProc);
  26162. Call.Data := Instance;
  26163. result := TStoredProc(Call);
  26164. end;
  26165. {$endif}
  26166. end;
  26167. function TPropInfo.GetterIsField: boolean;
  26168. begin
  26169. {$ifdef FPC}
  26170. result := PropProcs and 3=ptField;
  26171. {$else}
  26172. result := PropWrap(GetProc).Kind=$FF;
  26173. {$endif}
  26174. end;
  26175. function TPropInfo.SetterIsField: boolean;
  26176. begin
  26177. {$ifdef FPC}
  26178. result := (PropProcs shr 2) and 3=ptField;
  26179. {$else}
  26180. result := PropWrap(SetProc).Kind=$FF;
  26181. {$endif}
  26182. end;
  26183. function TPropInfo.WriteIsDefined: boolean;
  26184. begin
  26185. result := SetProc<>0;
  26186. end;
  26187. function TPropInfo.GetterAddr(Instance: pointer): pointer;
  26188. {$ifdef HASINLINE}
  26189. begin
  26190. result := Pointer(PtrInt(Instance)+GetProc{$ifndef FPC} and $00FFFFFF{$endif});
  26191. end;
  26192. {$else}
  26193. asm
  26194. mov eax,[eax].TPropInfo.GetProc
  26195. and eax,$00ffffff
  26196. add eax,edx
  26197. end;
  26198. {$endif}
  26199. function TPropInfo.SetterAddr(Instance: pointer): pointer;
  26200. begin
  26201. result := Pointer(PtrInt(Instance)+SetProc{$ifndef FPC} and $00FFFFFF{$endif});
  26202. end;
  26203. function TPropInfo.TypeInfo: PTypeInfo;
  26204. {$ifdef HASINLINE}
  26205. begin
  26206. {$ifndef HASDIRECTTYPEINFO}
  26207. if PropType<>nil then
  26208. result := PropType^ else
  26209. {$endif}
  26210. result := pointer(PropType);
  26211. end;
  26212. {$else}
  26213. asm // Delphi is so bad at compiling above code...
  26214. mov eax,[eax].TPropInfo.PropType
  26215. test eax,eax
  26216. jz @z
  26217. mov eax,[eax]
  26218. ret
  26219. @z: rep ret
  26220. end;
  26221. {$endif HASINLINE}
  26222. {$ifdef FPC_OR_PUREPASCAL}
  26223. function TPropInfo.Next: PPropInfo;
  26224. begin
  26225. result := AlignToPtr(@Name[ord(Name[0])+1]);
  26226. end;
  26227. {$else}
  26228. {$ifdef HASINLINE}
  26229. function TPropInfo.Next: PPropInfo;
  26230. begin
  26231. result := @Name[ord(Name[0])+1];
  26232. end;
  26233. {$else}
  26234. function TPropInfo.Next: PPropInfo;
  26235. asm // very fast code
  26236. movzx edx,byte ptr [eax].TPropInfo.Name
  26237. lea eax,[eax+edx].TPropInfo.Name[1]
  26238. end;
  26239. {$endif HASINLINE}
  26240. {$endif FPC_OR_PUREPASCAL}
  26241. {$ifdef USETYPEINFO}
  26242. function TPropInfo.GetOrdProp(Instance: TObject): PtrInt;
  26243. begin
  26244. result := TypInfo.GetOrdProp(Instance,@self);
  26245. end;
  26246. function TPropInfo.GetObjProp(Instance: TObject): TObject;
  26247. begin
  26248. if GetterIsField then
  26249. result := PObject(GetterAddr(Instance))^ else
  26250. result := pointer(TypInfo.GetOrdProp(Instance,@self));
  26251. end;
  26252. procedure TPropInfo.SetOrdProp(Instance: TObject; Value: PtrInt);
  26253. begin
  26254. if {$ifndef FPC}(PropType^.Kind=tkClass) and {$endif}
  26255. (SetProc=0) and GetterIsField then
  26256. // allow setting a class instance even if there is no "write ..." attribute
  26257. PPtrInt(GetterAddr(Instance))^ := Value else
  26258. TypInfo.SetOrdProp(Instance,@self,Value);
  26259. end;
  26260. function TPropInfo.GetInt64Prop(Instance: TObject): Int64;
  26261. begin
  26262. if GetterIsField then
  26263. result := PInt64(GetterAddr(Instance))^ else
  26264. result := TypInfo.GetInt64Prop(Instance,@self);
  26265. end;
  26266. procedure TPropInfo.SetInt64Prop(Instance: TObject; const Value: Int64);
  26267. begin
  26268. if SetterIsField then
  26269. PInt64(SetterAddr(Instance))^ := Value else
  26270. if (SetProc=0) and GetterIsField then
  26271. PInt64(GetterAddr(Instance))^ := Value else
  26272. TypInfo.SetInt64Prop(Instance,@self,Value);
  26273. end;
  26274. procedure TPropInfo.GetLongStrProp(Instance: TObject; var Value: RawByteString);
  26275. begin
  26276. {$ifdef UNICODE}
  26277. Value := TypInfo.GetAnsiStrProp(Instance,@self);
  26278. {$else}
  26279. Value := TypInfo.GetStrProp(Instance,@self);
  26280. {$endif}
  26281. end;
  26282. procedure TPropInfo.SetLongStrProp(Instance: TObject; const Value: RawByteString);
  26283. begin
  26284. {$ifdef UNICODE}
  26285. TypInfo.SetAnsiStrProp(Instance,@self,Value);
  26286. {$else}
  26287. TypInfo.SetStrProp(Instance,@self,Value);
  26288. {$endif}
  26289. end;
  26290. procedure TPropInfo.CopyLongStrProp(Source,Dest: TObject);
  26291. begin
  26292. {$ifdef UNICODE}
  26293. TypInfo.SetAnsiStrProp(Dest,@self,TypInfo.GetAnsiStrProp(Source,@self));
  26294. {$else}
  26295. SetStrProp(Dest,@self,TypInfo.GetStrProp(Source,@self));
  26296. {$endif}
  26297. end;
  26298. procedure TPropInfo.GetWideStrProp(Instance: TObject; var Value: WideString);
  26299. begin
  26300. Value := TypInfo.GetWideStrProp(Instance,@self);
  26301. end;
  26302. procedure TPropInfo.SetWideStrProp(Instance: TObject; const Value: WideString);
  26303. begin
  26304. TypInfo.SetWideStrProp(Instance,@self,Value);
  26305. end;
  26306. {$ifdef HASVARUSTRING}
  26307. function TPropInfo.GetUnicodeStrProp(Instance: TObject): UnicodeString;
  26308. begin
  26309. result := TypInfo.GetUnicodeStrProp(Instance,@self);
  26310. end;
  26311. procedure TPropInfo.SetUnicodeStrProp(Instance: TObject; const Value: UnicodeString);
  26312. begin
  26313. TypInfo.SetUnicodeStrProp(Instance,@self,Value);
  26314. end;
  26315. {$endif HASVARUSTRING}
  26316. function TPropInfo.GetCurrencyProp(Instance: TObject): currency;
  26317. begin
  26318. if GetterIsField then
  26319. result := PCurrency(GetterAddr(Instance))^ else
  26320. result := TypInfo.GetFloatProp(Instance,@self);
  26321. end;
  26322. procedure TPropInfo.SetCurrencyProp(Instance: TObject; const Value: Currency);
  26323. begin
  26324. if SetterIsField then
  26325. PCurrency(SetterAddr(Instance))^ := Value else
  26326. if (SetProc=0) and GetterIsField then
  26327. PCurrency(GetterAddr(Instance))^ := Value else
  26328. TypInfo.SetFloatProp(Instance,@self,value);
  26329. end;
  26330. function TPropInfo.GetDoubleProp(Instance: TObject): double;
  26331. begin
  26332. if GetterIsField then
  26333. result := PDouble(GetterAddr(Instance))^ else
  26334. result := TypInfo.GetFloatProp(Instance,@self);
  26335. end;
  26336. procedure TPropInfo.SetDoubleProp(Instance: TObject; Value: Double);
  26337. begin
  26338. if SetterIsField then
  26339. PDouble(SetterAddr(Instance))^ := Value else
  26340. if (SetProc=0) and GetterIsField then
  26341. PDouble(GetterAddr(Instance))^ := Value else
  26342. TypInfo.SetFloatProp(Instance,@self,value);
  26343. end;
  26344. function TPropInfo.GetFloatProp(Instance: TObject): double;
  26345. begin
  26346. result := TypInfo.GetFloatProp(Instance,@self);
  26347. end;
  26348. procedure TPropInfo.SetFloatProp(Instance: TObject; Value: TSynExtended);
  26349. begin
  26350. TypInfo.SetFloatProp(Instance,@self,value);
  26351. end;
  26352. {$ifndef NOVARIANTS}
  26353. procedure TPropInfo.GetVariantProp(Instance: TObject; var result: Variant);
  26354. begin
  26355. result := TypInfo.GetVariantProp(Instance,@self);
  26356. end;
  26357. procedure TPropInfo.SetVariantProp(Instance: TObject; const Value: Variant);
  26358. begin
  26359. if (SetProc=0) and GetterIsField then
  26360. PVariant(GetterAddr(Instance))^ := Value else
  26361. TypInfo.SetVariantProp(Instance,@self,Value);
  26362. end;
  26363. {$endif}
  26364. {$else USETYPEINFO}
  26365. function TPropInfo.GetOrdProp(Instance: TObject): PtrInt;
  26366. type // function(Instance: TObject) trick does not work with CPU64 :(
  26367. TGetProc = function: PtrInt of object;
  26368. TIndexedGetProc = function(Index: Integer): PtrInt of object;
  26369. var value: PtrInt;
  26370. Call: TMethod;
  26371. P: pointer;
  26372. begin
  26373. if GetProc=0 then // no read attribute -> use write offset
  26374. if PropWrap(SetProc).Kind<>$FF then begin
  26375. result := 0;
  26376. exit;
  26377. end else // we only allow setting if we know the field address
  26378. P := Pointer(PtrInt(Instance)+SetProc and $00FFFFFF) else
  26379. if PropWrap(GetProc).Kind=$FF then
  26380. P := Pointer(PtrInt(Instance)+GetProc and $00FFFFFF) else begin
  26381. if PropWrap(GetProc).Kind=$FE then
  26382. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26383. Call.Code := Pointer(GetProc);
  26384. Call.Data := Instance;
  26385. if Index=NO_INDEX then
  26386. value := TGetProc(Call) else
  26387. value := TIndexedGetProc(Call)(Index);
  26388. P := @value;
  26389. end;
  26390. with TypeInfo^ do
  26391. if Kind=tkClass then
  26392. result := PPtrInt(P)^ else
  26393. case TOrdType(PByte(AlignToPtr(@Name[ord(Name[0])+1]))^) of // OrdType of
  26394. otSByte: result := PShortInt(P)^;
  26395. otSWord: result := PSmallInt(P)^;
  26396. otSLong: result := PInteger(P)^;
  26397. otUByte: result := PByte(P)^;
  26398. otUWord: result := PWord(P)^;
  26399. otULong: result := PCardinal(P)^;
  26400. else result := 0; // should not happen
  26401. end;
  26402. end;
  26403. procedure TPropInfo.SetOrdProp(Instance: TObject; Value: PtrInt);
  26404. type // procedure(Instance: TObject) trick does not work with CPU64 :(
  26405. TSetProp = procedure(Value: PtrInt) of object;
  26406. TIndexedProp = procedure(Index: integer; Value: PtrInt) of object;
  26407. var P: pointer;
  26408. Call: TMethod;
  26409. begin
  26410. if SetProc=0 then // no write attribute -> use read offset
  26411. if PropWrap(GetProc).Kind<>$FF then
  26412. exit else // we only allow setting if we know the field address
  26413. P := Pointer(PtrInt(Instance)+GetProc and $00FFFFFF) else
  26414. if PropWrap(SetProc).Kind=$FF then
  26415. P := Pointer(PtrInt(Instance)+SetProc and $00FFFFFF) else begin
  26416. if PropWrap(SetProc).Kind=$FE then
  26417. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(SetProc))^ else
  26418. Call.Code := Pointer(SetProc);
  26419. Call.Data := Instance;
  26420. if Index=NO_INDEX then
  26421. TSetProp(Call)(Value) else
  26422. TIndexedProp(Call)(Index,Value);
  26423. exit;
  26424. end;
  26425. with PropType^^ do
  26426. if Kind=tkClass then
  26427. PPtrInt(P)^ := Value else
  26428. case TOrdType(PByte(AlignToPtr(@Name[ord(Name[0])+1]))^) of // OrdType of
  26429. otSByte: PShortInt(P)^ := Value;
  26430. otSWord: PSmallInt(P)^ := Value;
  26431. otSLong: PInteger(P)^ := Value;
  26432. otUByte: PByte(P)^ := Value;
  26433. otUWord: PWord(P)^ := Value;
  26434. otULong: PCardinal(P)^ := Value;
  26435. end;
  26436. end;
  26437. function TPropInfo.GetObjProp(Instance: TObject): TObject;
  26438. begin
  26439. if GetterIsField then
  26440. result := PObject(GetterAddr(Instance))^ else
  26441. result := pointer(GetOrdProp(Instance));
  26442. end;
  26443. function TPropInfo.GetInt64Prop(Instance: TObject): Int64;
  26444. type // function(Instance: TObject) trick does not work with CPU64 :(
  26445. TGetProc = function: Int64 of object;
  26446. TIndexedGetProc = function(Index: Integer): Int64 of object;
  26447. var Call: TMethod;
  26448. begin
  26449. if PropWrap(GetProc).Kind=$FF then
  26450. // field - Getter is the field offset in the instance data
  26451. result := PInt64(PtrInt(Instance)+GetProc and $00FFFFFF)^
  26452. else begin
  26453. if PropWrap(GetProc).Kind=$FE then
  26454. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26455. Call.Code := Pointer(GetProc);
  26456. Call.Data := Instance;
  26457. if Index=NO_INDEX then
  26458. result := TGetProc(Call) else
  26459. result := TIndexedGetProc(Call)(Index);
  26460. end;
  26461. end;
  26462. procedure TPropInfo.SetInt64Prop(Instance: TObject; const Value: Int64);
  26463. type // procedure(Instance: TObject) trick does not work with CPU64 :(
  26464. TSetProp = procedure(const Value: Int64) of object;
  26465. TIndexedProp = procedure(Index: integer; const Value: Int64) of object;
  26466. var Call: TMethod;
  26467. begin
  26468. if SetProc=0 then // no write attribute -> use read offset
  26469. if PropWrap(GetProc).Kind<>$FF then
  26470. exit else // we only allow setting if we know the field address
  26471. PInt64(PtrInt(Instance)+GetProc and $00FFFFFF)^ := Value else
  26472. if PropWrap(SetProc).Kind=$FF then
  26473. PInt64(PtrInt(Instance)+SetProc and $00FFFFFF)^ := Value else begin
  26474. if PropWrap(SetProc).Kind=$FE then
  26475. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(SetProc))^ else
  26476. Call.Code := Pointer(SetProc);
  26477. Call.Data := Instance;
  26478. if Index=NO_INDEX then
  26479. TSetProp(Call)(Value) else
  26480. TIndexedProp(Call)(Index,Value);
  26481. end;
  26482. end;
  26483. procedure TPropInfo.GetLongStrProp(Instance: TObject; var Value: RawByteString);
  26484. procedure CallMethod(Instance: TObject; var Value: RawByteString);
  26485. type // function(Instance: TObject) trick does not work with CPU64 :(
  26486. TAStringGetProc = function: RawByteString of object;
  26487. TAStringIndexedGetProc = function(Index: Integer): RawByteString of object;
  26488. var Call: TMethod;
  26489. begin
  26490. if PropWrap(GetProc).Kind=$FE then
  26491. // virtual method - Getter is a signed 2 byte integer VMT offset
  26492. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26493. // static method - Getter is the actual address
  26494. Call.Code := Pointer(GetProc);
  26495. Call.Data := Instance;
  26496. if Index=NO_INDEX then // no index
  26497. Value := TAStringGetProc(Call) else
  26498. Value := TAStringIndexedGetProc(Call)(Index);
  26499. end;
  26500. begin // caller must check that PropType^.Kind = tkWString
  26501. if PropWrap(GetProc).Kind=$FF then
  26502. // field - Getter is the field offset in the instance data
  26503. Value := PRawByteString(PtrInt(Instance)+GetProc and $00FFFFFF)^ else
  26504. CallMethod(Instance,Value);
  26505. end;
  26506. procedure TPropInfo.SetLongStrProp(Instance: TObject; const Value: RawByteString);
  26507. type // procedure(Instance: TObject) trick does not work with CPU64 :(
  26508. TSetProp = procedure(const Value: RawByteString) of object;
  26509. TIndexedProp = procedure(Index: integer; const Value: RawByteString) of object;
  26510. var Call: TMethod;
  26511. begin // caller must check that PropType^.Kind = tkLString
  26512. if SetProc=0 then // no setter ?
  26513. if PropWrap(GetProc).Kind<>$FF then
  26514. exit else // we only allow setting if we know the field address
  26515. PRawByteString(PtrInt(Instance)+GetProc and $00FFFFFF)^ := Value else
  26516. if PropWrap(SetProc).Kind=$FF then
  26517. // field - Setter is the field offset in the instance data
  26518. PRawByteString(PtrInt(Instance)+SetProc and $00FFFFFF)^ := Value else begin
  26519. if PropWrap(SetProc).Kind=$FE then
  26520. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(SetProc))^ else
  26521. Call.Code := Pointer(SetProc);
  26522. Call.Data := Instance;
  26523. if Index=NO_INDEX then
  26524. TSetProp(Call)(Value) else
  26525. TIndexedProp(Call)(Index,Value);
  26526. end;
  26527. end;
  26528. procedure TPropInfo.CopyLongStrProp(Source,Dest: TObject);
  26529. var tmp: RawByteString;
  26530. begin
  26531. GetLongStrProp(Source,tmp);
  26532. SetLongStrProp(Dest,tmp);
  26533. end;
  26534. procedure TPropInfo.GetWideStrProp(Instance: TObject; var Value: WideString);
  26535. type
  26536. TUStringGetProc = function: WideString of object;
  26537. TUStringIndexedGetProc = function(Index: Integer): WideString of object;
  26538. var M: TMethod;
  26539. begin // caller must check that PropType^.Kind = tkWString
  26540. if PropWrap(GetProc).Kind=$FF then
  26541. // field - Getter is the field offset in the instance data
  26542. Value := PWideString(PtrInt(Instance)+GetProc and $00FFFFFF)^
  26543. else begin
  26544. if PropWrap(GetProc).Kind=$FE then
  26545. // virtual method - Getter is a signed 2 byte integer VMT offset
  26546. M.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26547. // static method - Getter is the actual address
  26548. M.Code := Pointer(GetProc);
  26549. M.Data := Instance;
  26550. if Index=NO_INDEX then // no index
  26551. Value := TUStringGetProc(M)() else
  26552. Value := TUStringIndexedGetProc(M)(Index);
  26553. end;
  26554. end;
  26555. procedure TPropInfo.SetWideStrProp(Instance: TObject; const Value: WideString);
  26556. type
  26557. TUStringSetProc = procedure(const Value: WideString) of object;
  26558. TUStringIndexedSetProc = procedure(Index: Integer; const Value: WideString) of object;
  26559. var M: TMethod;
  26560. begin // caller must check that PropType^.Kind = tkWString
  26561. if SetProc=0 then // no setter ?
  26562. if PropWrap(GetProc).Kind<>$FF then
  26563. exit else begin // we only allow setting if we know the field address
  26564. PWideString(PtrInt(Instance)+GetProc and $00FFFFFF)^ := Value;
  26565. exit;
  26566. end;
  26567. if PropWrap(SetProc).Kind=$FF then
  26568. // field - Setter is the field offset in the instance data
  26569. PWideString(PtrInt(Instance)+SetProc and $00FFFFFF)^ := Value else begin
  26570. if PropWrap(SetProc).Kind=$FE then
  26571. // virtual method - Setter is a signed 2 byte integer VMT offset
  26572. M.Code := PPointer(PPtrInt(Instance)^+SmallInt(SetProc))^ else
  26573. // static method - Setter is the actual address
  26574. M.Code := Pointer(SetProc);
  26575. M.Data := Instance;
  26576. if Index=NO_INDEX then // no index
  26577. TUStringSetProc(M)(Value) else
  26578. TUStringIndexedSetProc(M)(Index, Value);
  26579. end;
  26580. end;
  26581. {$ifdef HASVARUSTRING}
  26582. function TPropInfo.GetUnicodeStrProp(Instance: TObject): UnicodeString;
  26583. type
  26584. TUStringGetProc = function: UnicodeString of object;
  26585. TUStringIndexedGetProc = function(Index: Integer): UnicodeString of object;
  26586. var M: TMethod;
  26587. begin // caller must check that PropType^.Kind = tkUString
  26588. if PropWrap(GetProc).Kind=$FF then
  26589. // field - Getter is the field offset in the instance data
  26590. result := PUnicodeString(PtrInt(Instance)+GetProc and $00FFFFFF)^
  26591. else begin
  26592. if PropWrap(GetProc).Kind=$FE then
  26593. // virtual method - Getter is a signed 2 byte integer VMT offset
  26594. M.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26595. // static method - Getter is the actual address
  26596. M.Code := Pointer(GetProc);
  26597. M.Data := Instance;
  26598. if Index=NO_INDEX then // no index
  26599. result := TUStringGetProc(M)() else
  26600. result := TUStringIndexedGetProc(M)(Index);
  26601. end;
  26602. end;
  26603. procedure TPropInfo.SetUnicodeStrProp(Instance: TObject; const Value: UnicodeString);
  26604. type
  26605. TUStringSetProc = procedure (const Value: UnicodeString) of object;
  26606. TUStringIndexedSetProc = procedure (Index: Integer; const Value: UnicodeString) of object;
  26607. var M: TMethod;
  26608. begin // caller must check that PropType^.Kind = tkUString
  26609. if SetProc=0 then // no setter ?
  26610. if PropWrap(GetProc).Kind<>$FF then
  26611. exit else begin // we only allow setting if we know the field address
  26612. PUnicodeString(PtrInt(Instance)+GetProc and $00FFFFFF)^ := Value;
  26613. exit;
  26614. end;
  26615. if PropWrap(SetProc).Kind=$FF then
  26616. // field - Setter is the field offset in the instance data
  26617. PUnicodeString(PtrInt(Instance)+SetProc and $00FFFFFF)^ := Value else begin
  26618. if PropWrap(SetProc).Kind=$FE then
  26619. // virtual method - Setter is a signed 2 byte integer VMT offset
  26620. M.Code := PPointer(PPtrInt(Instance)^+SmallInt(SetProc))^ else
  26621. // static method - Setter is the actual address
  26622. M.Code := Pointer(SetProc);
  26623. M.Data := Instance;
  26624. if Index=NO_INDEX then // no index
  26625. TUStringSetProc(M)(Value) else
  26626. TUStringIndexedSetProc(M)(Index, Value);
  26627. end;
  26628. end;
  26629. {$endif HASVARUSTRING}
  26630. function TPropInfo.GetCurrencyProp(Instance: TObject): currency;
  26631. type // function(Instance: TObject) trick does not work with CPU64 :(
  26632. TGetProc = function: currency of object;
  26633. TIndexedGetProc = function(Index: Integer): currency of object;
  26634. var P: Pointer;
  26635. Call: TMethod;
  26636. begin // faster code by AB
  26637. if PropWrap(GetProc).Kind=$FF then begin
  26638. // field - GetProc is the field offset in the instance data
  26639. P := Pointer(PtrInt(Instance)+GetProc and $00FFFFFF);
  26640. Result := PCurrency(P)^;
  26641. end
  26642. else begin
  26643. if PropWrap(GetProc).Kind=$FE then
  26644. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26645. Call.Code := Pointer(GetProc);
  26646. Call.Data := Instance;
  26647. if Index=NO_INDEX then
  26648. result := TGetProc(Call) else
  26649. result := TIndexedGetProc(Call)(Index);
  26650. end;
  26651. end;
  26652. procedure TPropInfo.SetCurrencyProp(Instance: TObject; const Value: Currency);
  26653. begin
  26654. if SetterIsField then
  26655. PCurrency(SetterAddr(Instance))^ := Value else
  26656. if (SetProc=0) and GetterIsField then
  26657. PCurrency(GetterAddr(Instance))^ := Value else
  26658. SetFloatProp(Instance,value);
  26659. end;
  26660. function TPropInfo.GetDoubleProp(Instance: TObject): double;
  26661. type // function(Instance: TObject) trick does not work with CPU64 :(
  26662. TGetProc = function: double of object;
  26663. TIndexedGetProc = function(Index: Integer): double of object;
  26664. var P: Pointer;
  26665. Call: TMethod;
  26666. begin // faster code by AB
  26667. if PropWrap(GetProc).Kind=$FF then begin
  26668. // field - GetProc is the field offset in the instance data
  26669. P := Pointer(PtrInt(Instance)+GetProc and $00FFFFFF);
  26670. Result := PDouble(P)^;
  26671. end
  26672. else begin
  26673. if PropWrap(GetProc).Kind=$FE then
  26674. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26675. Call.Code := Pointer(GetProc);
  26676. Call.Data := Instance;
  26677. if Index=NO_INDEX then
  26678. result := TGetProc(Call) else
  26679. result := TIndexedGetProc(Call)(Index);
  26680. end;
  26681. end;
  26682. procedure TPropInfo.SetDoubleProp(Instance: TObject; Value: Double);
  26683. begin
  26684. if SetterIsField then
  26685. PDouble(SetterAddr(Instance))^ := Value else
  26686. if (SetProc=0) and GetterIsField then
  26687. PDouble(GetterAddr(Instance))^ := Value else
  26688. SetFloatProp(Instance,value);
  26689. end;
  26690. function TPropInfo.GetFloatProp(Instance: TObject): double;
  26691. type // function(Instance: TObject) trick does not work with CPU64 :(
  26692. TGetProc = function: extended of object;
  26693. TIndexedGetProc = function(Index: Integer): extended of object;
  26694. var P: Pointer;
  26695. Call: TMethod;
  26696. begin // faster code by AB
  26697. if PropWrap(GetProc).Kind=$FF then begin
  26698. // field - GetProc is the field offset in the instance data
  26699. P := Pointer(PtrInt(Instance)+GetProc and $00FFFFFF);
  26700. case PropType^.FloatType of
  26701. ftSingle: Result := PSingle(P)^;
  26702. ftDoub: Result := PDouble(P)^;
  26703. ftExtended: Result := PExtended(P)^;
  26704. ftComp: Result := PComp(P)^;
  26705. ftCurr: Result := PCurrency(P)^; // use GetInt64Prop() to avoid rounding
  26706. else Result := 0;
  26707. end;
  26708. end
  26709. else begin
  26710. if PropWrap(GetProc).Kind=$FE then
  26711. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26712. Call.Code := Pointer(GetProc);
  26713. Call.Data := Instance;
  26714. if Index=NO_INDEX then
  26715. result := TGetProc(Call) else
  26716. result := TIndexedGetProc(Call)(Index);
  26717. if PropType^.FloatType = ftCurr then
  26718. Result := Result / 10000;
  26719. end;
  26720. end;
  26721. procedure TPropInfo.SetFloatProp(Instance: TObject; Value: TSynExtended);
  26722. type // procedure(Instance: TObject) trick does not work with CPU64 :(
  26723. TSingleSetProc = procedure(const Value: Single) of object;
  26724. TDoubleSetProc = procedure(const Value: Double) of object;
  26725. TExtendedSetProc = procedure(const Value: Extended) of object;
  26726. TCompSetProc = procedure(const Value: Comp) of object;
  26727. TCurrencySetProc = procedure(const Value: Currency) of object;
  26728. var P: Pointer;
  26729. Call: TMethod;
  26730. label St;
  26731. begin
  26732. if SetProc=0 then // no setter ?
  26733. if PropWrap(GetProc).Kind<>$FF then
  26734. exit else begin // we only allow setting if we know the field address
  26735. P := Pointer(PtrInt(Instance)+GetProc and $00FFFFFF);
  26736. goto St; // use the field address to set its value
  26737. end;
  26738. if PropWrap(SetProc).Kind=$FF then begin
  26739. // field - SetProc is the field offset in the instance data
  26740. P := Pointer(PtrInt(Instance)+SetProc and $00FFFFFF);
  26741. St: case PropType^^.FloatType of
  26742. ftSingle: PSingle(P)^ := Value;
  26743. ftDoub: PDouble(P)^ := Value;
  26744. ftExtended: PExtended(P)^ := Value;
  26745. ftComp: PComp(P)^ := Value;
  26746. ftCurr: PCurrency(P)^ := Value;
  26747. end;
  26748. end
  26749. else begin
  26750. if PropWrap(SetProc).Kind=$FE then
  26751. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(SetProc))^ else
  26752. Call.Code := Pointer(SetProc);
  26753. Call.Data := Instance;
  26754. if Index=NO_INDEX then begin // no index
  26755. case PropType^^.FloatType of
  26756. ftSingle : TSingleSetProc(Call)(Value);
  26757. ftDoub : TDoubleSetProc(Call)(Value);
  26758. ftExtended: TExtendedSetProc(Call)(Value);
  26759. ftComp : TCompSetProc(Call)(Value);
  26760. ftCurr : TCurrencySetProc(Call)(Value);
  26761. end;
  26762. end; // indexed methods not handled here, since not used in TSQLRecord
  26763. end;
  26764. end;
  26765. {$ifndef NOVARIANTS}
  26766. procedure TPropInfo.GetVariantProp(Instance: TObject; var result: Variant);
  26767. procedure ByMethod; // sub proc for faster execution of simple types
  26768. type // function(Instance: TObject) trick does not work with CPU64 :(
  26769. TGetProc = function: Variant of object;
  26770. TIndexedGetProc = function(Index: Integer): Variant of object;
  26771. var Call: TMethod;
  26772. begin
  26773. if PropWrap(GetProc).Kind=$FE then
  26774. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(GetProc))^ else
  26775. Call.Code := Pointer(GetProc);
  26776. Call.Data := Instance;
  26777. if Index=NO_INDEX then
  26778. result := TGetProc(Call) else
  26779. result := TIndexedGetProc(Call)(Index);
  26780. end;
  26781. begin
  26782. if PropWrap(GetProc).Kind=$FF then
  26783. // field - Getter is the field offset in the instance data
  26784. SetVariantByValue(PVariant(PtrInt(Instance)+GetProc and $00FFFFFF)^,result) else
  26785. ByMethod;
  26786. end;
  26787. procedure TPropInfo.SetVariantProp(Instance: TObject; const Value: Variant);
  26788. procedure ByMethod; // sub proc for faster execution of simple types
  26789. type // procedure(Instance: TObject) trick does not work with CPU64 :(
  26790. TSetProp = procedure(const Value: Variant) of object;
  26791. TIndexedProp = procedure(Index: integer; const Value: Variant) of object;
  26792. var Call: TMethod;
  26793. begin
  26794. if PropWrap(SetProc).Kind=$FE then
  26795. Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(SetProc))^ else
  26796. Call.Code := Pointer(SetProc);
  26797. Call.Data := Instance;
  26798. if Index=NO_INDEX then
  26799. TSetProp(Call)(Value) else
  26800. TIndexedProp(Call)(Index,Value);
  26801. end;
  26802. begin
  26803. if SetProc=0 then // no write attribute -> use read offset
  26804. if PropWrap(GetProc).Kind<>$FF then
  26805. exit else // we only allow setting if we know the field address
  26806. PVariant(PtrInt(Instance)+GetProc and $00FFFFFF)^ := Value else
  26807. if PropWrap(SetProc).Kind=$FF then
  26808. PVariant(PtrInt(Instance)+SetProc and $00FFFFFF)^ := Value else
  26809. ByMethod;
  26810. end;
  26811. {$endif}
  26812. {$endif USETYPEINFO}
  26813. type
  26814. TIntfFlag = (ifHasGuid,ifDispInterface,ifDispatch{$ifdef FPC},ifHasStrGUID{$endif});
  26815. TIntfFlags = set of TIntfFlag;
  26816. {$ifdef FPC}
  26817. {$PACKRECORDS C}
  26818. {$endif}
  26819. PInterfaceTypeData = ^TInterfaceTypeData;
  26820. TInterfaceTypeData =
  26821. {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}packed{$endif} record
  26822. IntfParent: PPTypeInfo; // ancestor
  26823. IntfFlags: TIntfFlags;
  26824. IntfGuid: TGUID;
  26825. IntfUnit: ShortString;
  26826. end;
  26827. {$ifdef FPC}
  26828. PRawInterfaceTypeData = ^TRawInterfaceTypeData;
  26829. TRawInterfaceTypeData =
  26830. {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}packed{$endif} record
  26831. RawIntfParent: PTypeInfo;
  26832. RawIntfFlags : TIntfFlagsBase;
  26833. IID: TGUID;
  26834. RawIntfUnit: ShortString;
  26835. IIDStr: ShortString;
  26836. end;
  26837. {$endif}
  26838. {$ifdef FPC}
  26839. {$PACKRECORDS DEFAULT}
  26840. {$endif}
  26841. TMethodKind = (mkProcedure, mkFunction, mkConstructor, mkDestructor,
  26842. mkClassProcedure, mkClassFunction, { Obsolete } mkSafeProcedure, mkSafeFunction);
  26843. TIntfMethodEntryTail =
  26844. {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}packed{$endif} record
  26845. {$ifdef FPC}
  26846. Version: Byte; // alwyas 3 at the moment
  26847. {$else}
  26848. Kind: TMethodKind;
  26849. {$endif}
  26850. CC: TCallingConvention;
  26851. {$ifdef FPC}
  26852. ResultType: PTypeInfo;
  26853. StackSize: Word;
  26854. {$endif}
  26855. ParamCount: Byte;
  26856. {Params: array[0..ParamCount - 1] of TVmtMethodParam;}
  26857. end;
  26858. { TTypeInfo }
  26859. {$ifdef HASINLINE}
  26860. function TTypeInfo.ClassType: PClassType;
  26861. begin
  26862. result := AlignToPtr(@Name[ord(Name[0])+1]);
  26863. end;
  26864. {$else}
  26865. function TTypeInfo.ClassType: PClassType;
  26866. asm // very fast code
  26867. movzx edx,byte ptr [eax].TTypeInfo.Name
  26868. lea eax,[eax+edx].TTypeInfo.Name[1]
  26869. end;
  26870. {$endif}
  26871. function TTypeInfo.ClassCreate: TObject;
  26872. var instance: TClassInstance;
  26873. begin
  26874. instance.Init(ClassType^.ClassType);
  26875. result := instance.CreateNew;
  26876. end;
  26877. function TTypeInfo.RecordType: PRecordType;
  26878. {$ifdef HASINLINE}
  26879. begin
  26880. result := AlignToPtr(@Name[ord(Name[0])+1]);
  26881. {$else}
  26882. asm // very fast code
  26883. movzx edx,byte ptr [eax].TTypeInfo.Name
  26884. lea eax,[eax+edx].TTypeInfo.Name[1]
  26885. {$endif}
  26886. end;
  26887. function TTypeInfo.ClassFieldCount(onlyWithoutGetter: boolean): integer;
  26888. begin
  26889. result := ClassFieldCountWithParents(ClassType^.ClassType,onlyWithoutGetter);
  26890. end;
  26891. function TTypeInfo.ClassSQLFieldType: TSQLFieldType;
  26892. var CT: PClassType;
  26893. C,C2: TClass;
  26894. begin
  26895. CT := AlignToPtr(@Name[ord(Name[0])+1]); // inlined ClassType
  26896. C := CT^.ClassType;
  26897. C2 := C;
  26898. while true do // unrolled several InheritsFrom() calls
  26899. if C<>TSQLRecordMany then
  26900. if C<>TSQLRecord then
  26901. if (C<>TRawUTF8List) and (C<>TStrings) and
  26902. (C<>TObjectList) {$ifndef LVCL}and (C<>TCollection){$endif} then
  26903. if CT^.ParentInfo<>nil then begin
  26904. with Deref(CT^.ParentInfo)^ do
  26905. CT := AlignToPtr(@Name[ord(Name[0])+1]); // get parent ClassType
  26906. C := CT^.ClassType;
  26907. if C<>TObject then
  26908. continue else
  26909. break;
  26910. end else break
  26911. else begin
  26912. result := sftObject; // TStrings, TRawUTF8List or TCollection
  26913. exit;
  26914. end else begin
  26915. result := sftID; // TSQLRecord field is pointer(RecordID), not an Instance
  26916. exit;
  26917. end else begin
  26918. result := sftMany; // no data is stored here, but in a pivot table
  26919. exit;
  26920. end;
  26921. if ClassHasPublishedFields(C2) then
  26922. result := sftObject else // identify any class with published properties
  26923. result := sftUnknown;
  26924. end;
  26925. function TTypeInfo.EnumBaseType: PEnumType;
  26926. {$ifdef HASINLINE}
  26927. begin
  26928. {$ifdef FPC}
  26929. result := pointer(GetFPCTypeData(@Self));
  26930. {$else}
  26931. with PEnumType(@Name[ord(Name[0])+1])^.BaseType^^ do
  26932. result := @Name[ord(Name[0])+1];
  26933. {$endif}
  26934. {$else}
  26935. asm // very fast code
  26936. movzx edx,byte ptr [eax].TTypeInfo.Name
  26937. mov eax,[eax+edx].TTypeInfo.Name[1].TEnumType.BaseType
  26938. mov eax,[eax]
  26939. movzx edx,byte ptr [eax].TTypeInfo.Name
  26940. lea eax,[eax+edx].TTypeInfo.Name[1]
  26941. {$endif}
  26942. end;
  26943. function TTypeInfo.InheritsFrom(AClass: TClass): boolean;
  26944. {$ifdef FPC_OR_PUREPASCAL}
  26945. var CT: PClassType;
  26946. begin
  26947. CT := ClassType;
  26948. repeat
  26949. if CT^.ClassType={$ifndef FPC}pointer{$endif}(AClass) then begin
  26950. result := true;
  26951. exit;
  26952. end;
  26953. if CT^.ParentInfo = nil then
  26954. break else
  26955. CT := CT^.ParentInfo^.ClassType;
  26956. until CT = nil;
  26957. result := false;
  26958. end;
  26959. {$else}
  26960. asm // eax=PClassType edx=AClass
  26961. @1:movzx ecx,byte ptr [eax].TTypeInfo.Name
  26962. lea eax,[eax+ecx].TTypeInfo.Name[1]
  26963. cmp edx,[eax].TClassType.ClassType
  26964. jz @2
  26965. mov eax,[eax].TClassType.ParentInfo
  26966. test eax,eax
  26967. jz @3 // no parent
  26968. mov eax,[eax] // get parent type info
  26969. jmp @1
  26970. @3:rep ret
  26971. @2:mov eax,1
  26972. end;
  26973. {$endif}
  26974. function TTypeInfo.GetSQLFieldType: TSQLFieldType;
  26975. begin // very fast, thanks to the TypeInfo() compiler-generated function
  26976. case Kind of
  26977. tkInteger: begin
  26978. result := sftInteger;
  26979. exit; // direct exit is faster in generated asm code (Delphi 7 at least)
  26980. end;
  26981. tkInt64:
  26982. if (@self=TypeInfo(TRecordReference)) or
  26983. (@self=TypeInfo(TRecordReferenceToBeDeleted)) then begin
  26984. result := sftRecord;
  26985. exit;
  26986. end else
  26987. if @self=TypeInfo(TCreateTime) then begin
  26988. result := sftCreateTime;
  26989. exit;
  26990. end else
  26991. if @self=TypeInfo(TModTime) then begin
  26992. result := sftModTime;
  26993. exit;
  26994. end else
  26995. if @self=TypeInfo(TTimeLog) then begin
  26996. result := sftTimeLog;
  26997. exit;
  26998. end else
  26999. if @self=TypeInfo(TID) then begin
  27000. result := sftTID;
  27001. exit;
  27002. end else
  27003. if @self=TypeInfo(TSessionUserID) then begin
  27004. result := sftSessionUserID;
  27005. exit;
  27006. end else
  27007. if @self=TypeInfo(TRecordVersion) then begin
  27008. result := sftRecordVersion;
  27009. exit;
  27010. end else
  27011. if (ord(Name[1]) and $df=ord('T')) and // T...ID pattern in type name -> TID
  27012. (PWord(@Name[ord(Name[0])-1])^ and $dfdf=ord('I')+ord('D') shl 8) then begin
  27013. result := sftTID;
  27014. exit;
  27015. end else begin
  27016. result := sftInteger;
  27017. exit;
  27018. end;
  27019. {$ifdef FPC}
  27020. tkBool: begin
  27021. result := sftBoolean;
  27022. exit;
  27023. end;
  27024. {$endif}
  27025. tkSet: begin
  27026. result := sftSet;
  27027. exit;
  27028. end;
  27029. tkEnumeration:
  27030. {$ifndef FPC}
  27031. if @self=TypeInfo(Boolean) then begin
  27032. result := sftBoolean;
  27033. exit;
  27034. end else
  27035. {$endif}
  27036. if @self=TypeInfo(WordBool) then begin // circumvent a Delphi RTTI bug
  27037. result := sftBoolean;
  27038. exit;
  27039. end else
  27040. begin
  27041. result := sftEnumerate;
  27042. exit;
  27043. end;
  27044. tkFloat:
  27045. if @self=TypeInfo(Currency) then begin
  27046. result := sftCurrency;
  27047. exit;
  27048. end else
  27049. if @self=TypeInfo(TDateTime) then begin
  27050. result := sftDateTime;
  27051. exit;
  27052. end else begin
  27053. result := sftFloat;
  27054. exit;
  27055. end;
  27056. {$ifdef FPC}tkAString,{$endif} tkLString:
  27057. // do not use AnsiStringCodePage since AnsiString = GetAcp may change
  27058. if (@self=TypeInfo(TSQLRawBlob)) or
  27059. (@self=TypeInfo(RawByteString)) then begin
  27060. result := sftBlob;
  27061. exit;
  27062. end else
  27063. if @self=TypeInfo(WinAnsiString) then begin
  27064. result := sftAnsiText;
  27065. exit;
  27066. end else begin
  27067. result := sftUTF8Text; // CP_UTF8,CP_UTF16 and any other to UTF-8 text
  27068. exit;
  27069. end;
  27070. {$ifdef HASVARUSTRING}tkUString,{$endif} tkChar, tkWChar, tkWString: begin
  27071. result := sftUTF8Text;
  27072. exit;
  27073. end;
  27074. tkDynArray: begin
  27075. result := sftBlobDynArray;
  27076. exit;
  27077. end;
  27078. {$ifdef PUBLISHRECORD}
  27079. tkRecord{$ifdef FPC},tkObject{$endif}: begin
  27080. result := sftUTF8Custom;
  27081. exit;
  27082. end;
  27083. {$endif}
  27084. {$ifndef NOVARIANTS}
  27085. tkVariant: begin // this function does not need to handle sftNullable
  27086. result := sftVariant;
  27087. exit;
  27088. end;
  27089. {$endif}
  27090. tkClass: begin
  27091. result := ClassSQLFieldType;
  27092. exit;
  27093. end;
  27094. // note: tkString (shortstring) and tkInterface not handled
  27095. else begin
  27096. result := sftUnknown;
  27097. exit;
  27098. end;
  27099. end;
  27100. end;
  27101. function TTypeInfo.FloatType: TFloatType;
  27102. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27103. var
  27104. td: PTypeData;
  27105. {$endif}
  27106. begin
  27107. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27108. td := GetTypeData(@Self);
  27109. result := TFloatType(PByte(td)^);
  27110. {$else}
  27111. result := TFloatType(PByte(@Name[ord(Name[0])+1])^);
  27112. {$endif}
  27113. end;
  27114. function TTypeInfo.OrdType: TOrdType;
  27115. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27116. var
  27117. td: PTypeData;
  27118. {$endif}
  27119. begin
  27120. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27121. td := GetTypeData(@Self);
  27122. result := TOrdType(PByte(td)^);
  27123. {$else}
  27124. result := TOrdType(PByte(@Name[ord(Name[0])+1])^);
  27125. {$endif}
  27126. end;
  27127. function TTypeInfo.SetEnumType: PEnumType;
  27128. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27129. var p: pointer;
  27130. begin
  27131. if (@self=nil) or (Kind<>tkSet) then
  27132. result := nil else begin
  27133. p := pointer(GetTypeData(@Self));
  27134. inc(p,sizeof(TOrdType));
  27135. p := AlignToPtr(p);
  27136. result := PPTypeInfo(PPointer(p)^)^.EnumBaseType;
  27137. end;
  27138. {$else}
  27139. begin
  27140. if (@self=nil) or (Kind<>tkSet) then
  27141. result := nil else
  27142. result := PPTypeInfo(PPointer(PtrUInt(@Name[ord(Name[0])+1])+sizeof(TOrdType))^)^.
  27143. EnumBaseType;
  27144. {$endif}
  27145. end;
  27146. function TTypeInfo.DynArrayItemType(aDataSize: PInteger): PTypeInfo;
  27147. begin
  27148. if @self=nil then
  27149. result := nil else
  27150. result := DynArrayTypeInfoToRecordInfo(@self,aDataSize);
  27151. end;
  27152. function TTypeInfo.DynArrayItemSize: integer;
  27153. begin
  27154. if @self=nil then
  27155. result := 0 else
  27156. DynArrayTypeInfoToRecordInfo(@self,@result);
  27157. end;
  27158. function TTypeInfo.AnsiStringCodePage: integer;
  27159. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27160. var
  27161. td: PTypeData;
  27162. {$endif}
  27163. begin
  27164. {$ifdef HASCODEPAGE}
  27165. if @self=TypeInfo(TSQLRawBlob) then
  27166. result := CP_SQLRAWBLOB else
  27167. if Kind in [{$ifdef FPC}tkAString,{$endif} tkLString] then
  27168. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27169. begin
  27170. td := GetTypeData(@Self);
  27171. result := PWORD(td)^;
  27172. end else
  27173. {$else}
  27174. result := PWord(@Name[ord(Name[0])+1])^ else // from RTTI
  27175. {$endif}
  27176. {$else}
  27177. if @self=TypeInfo(RawUTF8) then
  27178. result := CP_UTF8 else
  27179. if @self=TypeInfo(WinAnsiString) then
  27180. result := CODEPAGE_US else
  27181. if @self=TypeInfo(RawUnicode) then
  27182. result := CP_UTF16 else
  27183. if @self=TypeInfo(TSQLRawBlob) then
  27184. result := CP_SQLRAWBLOB else
  27185. if @self=TypeInfo(RawByteString) then
  27186. result := CP_RAWBYTESTRING else
  27187. if (@self=TypeInfo(AnsiString)) or IdemPropName(Name,'TCaption') then
  27188. result := 0 else
  27189. {$endif}
  27190. result := CP_UTF8; // default is UTF-8
  27191. end;
  27192. function TTypeInfo.InterfaceGUID: PGUID;
  27193. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27194. var
  27195. td:PTypeData;
  27196. {$endif}
  27197. begin
  27198. if (@self=nil) or (Kind<>tkInterface) then result := nil else
  27199. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27200. begin
  27201. td := GetTypeData(@Self);
  27202. result := @td^.GUID;
  27203. end;
  27204. {$else}
  27205. result := @PInterfaceTypeData(@Name[ord(Name[0])+1])^.IntfGuid;
  27206. {$endif}
  27207. end;
  27208. function TTypeInfo.InterfaceUnitName: PShortString;
  27209. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27210. var
  27211. td: PTypeData;
  27212. {$endif}
  27213. begin
  27214. if (@self=nil) or (Kind<>tkInterface) then
  27215. result := @NULL_SHORTSTRING else
  27216. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27217. begin
  27218. td := GetTypeData(@Self);
  27219. result := @td^.IntfUnit;
  27220. end;
  27221. {$else}
  27222. result := @PInterfaceTypeData(@Name[ord(Name[0])+1])^.IntfUnit;
  27223. {$endif}
  27224. end;
  27225. function TTypeInfo.InterfaceAncestor: PTypeInfo;
  27226. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27227. var
  27228. td: PTypeData;
  27229. {$endif}
  27230. begin
  27231. if (@self=nil) or (Kind<>tkInterface) then
  27232. result := nil else
  27233. begin
  27234. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27235. td := GetTypeData(@Self);
  27236. with td^ do
  27237. {$else}
  27238. with PInterfaceTypeData(@Name[ord(Name[0])+1])^ do
  27239. {$endif}
  27240. if IntfParent=nil then
  27241. result := nil else
  27242. result := mORMot.PTypeInfo(Deref(IntfParent));
  27243. end;
  27244. end;
  27245. procedure TTypeInfo.InterfaceAncestors(out Ancestors: PTypeInfoDynArray;
  27246. OnlyImplementedBy: TInterfacedObjectClass;
  27247. out AncestorsImplementedEntry: TPointerDynArray);
  27248. var n: integer;
  27249. nfo: PTypeInfo;
  27250. typ: PInterfaceTypeData;
  27251. entry: pointer;
  27252. begin
  27253. if (@self=nil) or (Kind<>tkInterface) then
  27254. exit;
  27255. n := 0;
  27256. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27257. typ := PInterfaceTypeData(GetTypeData(@Self));
  27258. {$else}
  27259. typ := @Name[ord(Name[0])+1];
  27260. {$endif}
  27261. repeat
  27262. if typ^.IntfParent=nil then
  27263. exit;
  27264. nfo := Deref(typ^.IntfParent);
  27265. if nfo=TypeInfo(IInterface) then
  27266. exit;
  27267. {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
  27268. typ := AlignToPtr(@nfo^.Name[ord(nfo^.Name[0])+1]);
  27269. {$else}
  27270. typ := @nfo^.Name[ord(nfo^.Name[0])+1];
  27271. {$endif}
  27272. if ifHasGuid in typ^.IntfFlags then begin
  27273. if OnlyImplementedBy<>nil then begin
  27274. entry := OnlyImplementedBy.GetInterfaceEntry(typ^.IntfGuid);
  27275. if entry=nil then
  27276. continue;
  27277. Setlength(AncestorsImplementedEntry,n+1);
  27278. AncestorsImplementedEntry[n] := entry;
  27279. end;
  27280. SetLength(Ancestors,n+1);
  27281. Ancestors[n] := nfo;
  27282. inc(n);
  27283. end;
  27284. until false;
  27285. end;
  27286. { TClassProp }
  27287. function TClassProp.FieldProp(const PropName: shortstring): PPropInfo;
  27288. var i: integer;
  27289. begin
  27290. if @self<>nil then begin
  27291. result := @PropList;
  27292. for i := 1 to PropCount do
  27293. if IdemPropName(result^.Name,PropName) then
  27294. exit else
  27295. result := result^.Next;
  27296. end;
  27297. result := nil;
  27298. end;
  27299. { TClassType }
  27300. {$ifdef FPC}
  27301. function TClassType.ClassProp: PClassProp;
  27302. begin
  27303. if pointer(@self)<>nil then
  27304. result := AlignToPtr(@UnitName[ord(UnitName[0])+1]) else
  27305. result := nil; // avoid GPF
  27306. end;
  27307. {$else}
  27308. function TClassType.ClassProp: PClassProp;
  27309. begin
  27310. if pointer(@self)<>nil then
  27311. result := pointer(@UnitName[ord(UnitName[0])+1]) else
  27312. result := nil; // avoid GPF
  27313. end;
  27314. {$endif}
  27315. function TClassType.RTTISize: integer;
  27316. var C: PClassProp;
  27317. P: PPropInfo;
  27318. i: Integer;
  27319. begin
  27320. result := 0;
  27321. C := ClassProp;
  27322. if C=nil then
  27323. exit;
  27324. P := @C^.PropList;
  27325. for i := 1 to C^.PropCount do
  27326. P := P^.Next;
  27327. result := PtrUInt(P)-PtrUInt(@self);
  27328. end;
  27329. {$ifdef PUREPASCAL}
  27330. function TClassType.InheritsFrom(AClass: TClass): boolean;
  27331. var P: PTypeInfo;
  27332. begin
  27333. result := true;
  27334. if ClassType=AClass then
  27335. exit;
  27336. P := DeRef(ParentInfo);
  27337. while P<>nil do
  27338. with P^.ClassType^ do
  27339. if ClassType=AClass then
  27340. exit else
  27341. P := DeRef(ParentInfo);
  27342. result := false;
  27343. end;
  27344. {$else}
  27345. function TClassType.InheritsFrom(AClass: TClass): boolean;
  27346. asm // eax=PClassType edx=AClass
  27347. cmp [eax].TClassType.ClassType,edx
  27348. jz @3
  27349. @2: mov eax,[eax].TClassType.ParentInfo
  27350. test eax,eax
  27351. jz @0
  27352. @1: mov eax,[eax]
  27353. movzx ecx,byte ptr [eax].TTypeInfo.Name
  27354. lea eax,[eax+ecx].TTypeInfo.Name[1]
  27355. cmp edx,[eax].TClassType.ClassType
  27356. jnz @2
  27357. @3: mov eax,1
  27358. @0:
  27359. end;
  27360. {$endif}
  27361. { TEnumType }
  27362. {$ifdef FPC_ENUMHASINNER}
  27363. function TEnumType.MinValue: Longint;
  27364. begin
  27365. result := inner.iMinValue;
  27366. end;
  27367. function TEnumType.MaxValue: Longint;
  27368. begin
  27369. result := inner.iMaxValue;
  27370. end;
  27371. function TEnumType.BaseType: PPTypeInfo;
  27372. begin
  27373. result := inner.iBaseType;
  27374. end;
  27375. {$endif FPC_ENUMHASINNER}
  27376. function TEnumType.GetEnumName(const Value): PShortString;
  27377. var Ordinal: integer;
  27378. begin
  27379. case OrdType of // MaxValue does not work e.g. with WordBool
  27380. otSByte, otUByte: Ordinal := byte(Value);
  27381. otSWord, otUWord: Ordinal := word(Value);
  27382. else Ordinal := integer(Value);
  27383. end;
  27384. result := GetEnumNameOrd(Ordinal);
  27385. end;
  27386. function TEnumType.GetEnumNameOrd(Value: Integer): PShortString;
  27387. // note: FPC doesn't align NameList (cf. GetEnumName() function in typinfo.pp)
  27388. {$ifdef PUREPASCAL}
  27389. begin
  27390. result := @NameList;
  27391. if cardinal(Value)<=cardinal(MaxValue) then
  27392. while Value>0 do begin
  27393. dec(Value);
  27394. inc(PByte(result),ord(result^[0])+1);
  27395. end else
  27396. result := @NULL_SHORTSTRING;
  27397. end;
  27398. {$else}
  27399. asm // eax=PEnumType edx=Value
  27400. xor ecx,ecx
  27401. {$ifdef FPC_ENUMHASINNER}
  27402. cmp edx,[eax].TEnumType.inner.iMaxValue
  27403. {$else}
  27404. cmp edx,[eax].TEnumType.MaxValue
  27405. {$endif}
  27406. lea eax,[eax].TEnumType.NameList
  27407. ja @0
  27408. test edx,edx
  27409. jz @z
  27410. push edx
  27411. shr edx,2 // fast pipelined by-four scanning
  27412. jz @1
  27413. @4: dec edx
  27414. movzx ecx,byte ptr [eax]
  27415. lea eax,[eax+ecx+1]
  27416. movzx ecx,byte ptr [eax]
  27417. lea eax,[eax+ecx+1]
  27418. movzx ecx,byte ptr [eax]
  27419. lea eax,[eax+ecx+1]
  27420. movzx ecx,byte ptr [eax]
  27421. lea eax,[eax+ecx+1]
  27422. jnz @4
  27423. pop edx
  27424. and edx,3
  27425. jnz @s
  27426. @z: ret
  27427. @1: pop edx
  27428. @s: movzx ecx,byte ptr [eax]
  27429. dec edx
  27430. lea eax,[eax+ecx+1] // next short string
  27431. jnz @s
  27432. ret
  27433. @0: lea eax,NULL_SHORTSTRING
  27434. end;
  27435. {$endif}
  27436. function TEnumType.GetSetNameCSV(Value: integer; SepChar: AnsiChar;
  27437. FullSetsAsStar: boolean): RawUTF8;
  27438. var W: TTextWriter;
  27439. begin
  27440. W := TTextWriter.CreateOwnedStream(1024);
  27441. try
  27442. GetSetNameCSV(W,Value,SepChar,FullSetsAsStar);
  27443. W.SetText(result);
  27444. finally
  27445. W.Free;
  27446. end;
  27447. end;
  27448. procedure TEnumType.GetSetNameCSV(W: TTextWriter; Value: integer; SepChar: AnsiChar;
  27449. FullSetsAsStar: boolean);
  27450. var j: integer;
  27451. PS: PShortString;
  27452. begin
  27453. W.Add('[');
  27454. if FullSetsAsStar and (MaxValue<32) and
  27455. GetAllBits(Value,MaxValue+1) then
  27456. W.AddShort('"*"') else begin
  27457. PS := @NameList;
  27458. for j := MinValue to MaxValue do begin
  27459. if GetBit(Value,j) then begin
  27460. W.Add('"');
  27461. if twoTrimLeftEnumSets in W.CustomOptions then
  27462. W.AddTrimLeftLowerCase(PS) else
  27463. W.AddShort(PS^);
  27464. W.Add('"',SepChar);
  27465. end;
  27466. inc(PByte(PS),ord(PS^[0])+1); // next item
  27467. end;
  27468. end;
  27469. W.CancelLastComma;
  27470. W.Add(']');
  27471. end;
  27472. function TEnumType.GetSetNameAsDocVariant(Value: integer; FullSetsAsStar: boolean): variant;
  27473. var j: integer;
  27474. PS: PShortString;
  27475. arr: TDocVariantData;
  27476. begin
  27477. arr.InitFast;
  27478. if FullSetsAsStar and (MaxValue<32) and
  27479. GetAllBits(Value,MaxValue+1) then
  27480. arr.AddItem('*') else begin
  27481. PS := @NameList;
  27482. for j := MinValue to MaxValue do begin
  27483. if GetBit(Value,j) then
  27484. arr.AddItem(PS^);
  27485. inc(PByte(PS),ord(PS^[0])+1); // next item
  27486. end;
  27487. end;
  27488. result := variant(arr);
  27489. end;
  27490. function TEnumType.GetEnumNameValue(Value: PUTF8Char; ValueLen: integer;
  27491. AlsoTrimLowerCase: boolean): Integer;
  27492. begin
  27493. if (Value<>nil) and (ValueLen>0) then begin
  27494. result := FindShortStringListExact(@NameList,
  27495. MaxValue,Value,ValueLen);
  27496. if (result<0) and AlsoTrimLowerCase then
  27497. result := FindShortStringListTrimLowerCase(@NameList,
  27498. MaxValue,Value,ValueLen);
  27499. end else
  27500. result := -1;
  27501. end;
  27502. function TEnumType.GetEnumNameValue(const EnumName: ShortString): Integer;
  27503. begin
  27504. result := GetEnumNameValue(@EnumName[1],ord(EnumName[0]));
  27505. end;
  27506. function TEnumType.GetEnumNameValue(Value: PUTF8Char): Integer;
  27507. begin
  27508. result := GetEnumNameValue(Value,StrLen(Value));
  27509. end;
  27510. {$ifdef HASINLINE}
  27511. function TEnumType.GetEnumNameTrimed(const Value): RawUTF8;
  27512. begin
  27513. result := TrimLeftLowerCaseShort(GetEnumName(Value));
  27514. end;
  27515. {$else}
  27516. {$ifdef PUREPASCAL}
  27517. function TEnumType.GetEnumNameTrimed(const Value): RawUTF8;
  27518. begin
  27519. result := TrimLeftLowerCaseShort(GetEnumName(Value));
  27520. end;
  27521. {$else}
  27522. function TEnumType.GetEnumNameTrimed(const Value): RawUTF8;
  27523. asm
  27524. push ecx
  27525. call TEnumType.GetEnumName
  27526. pop edx
  27527. jmp TrimLeftLowerCaseShort
  27528. end;
  27529. {$endif}
  27530. {$endif}
  27531. function TEnumType.GetCaption(const Value): string;
  27532. // GetCaptionFromPCharLen() expect ASCIIz -> use temp RawUTF8
  27533. begin
  27534. GetCaptionFromPCharLen(pointer(GetEnumNameTrimed(Value)),result);
  27535. end;
  27536. procedure TEnumType.GetEnumNameTrimedAll(var result: RawUTF8; const Prefix: RawUTF8;
  27537. quotedValues: boolean);
  27538. var i: integer;
  27539. V: PShortString;
  27540. begin
  27541. with TTextWriter.CreateOwnedStream(1024) do
  27542. try
  27543. AddString(Prefix);
  27544. V := @NameList;
  27545. for i := MinValue to MaxValue do begin
  27546. if quotedValues then
  27547. Add('"');
  27548. AddTrimLeftLowerCase(V);
  27549. if quotedValues then
  27550. Add('"');
  27551. Add(',');
  27552. inc(PByte(V),length(V^)+1);
  27553. end;
  27554. CancelLastComma;
  27555. SetText(result);
  27556. finally
  27557. Free;
  27558. end;
  27559. end;
  27560. procedure TEnumType.GetEnumNameAll(var result: RawUTF8; const Prefix: RawUTF8;
  27561. quotedValues: boolean);
  27562. var i: integer;
  27563. V: PShortString;
  27564. begin
  27565. with TTextWriter.CreateOwnedStream(1024) do
  27566. try
  27567. AddString(Prefix);
  27568. V := @NameList;
  27569. for i := MinValue to MaxValue do begin
  27570. if quotedValues then
  27571. Add('"');
  27572. if twoTrimLeftEnumSets in CustomOptions then
  27573. AddTrimLeftLowerCase(V) else
  27574. AddShort(V^);
  27575. if quotedValues then
  27576. Add('"');
  27577. Add(',');
  27578. inc(PByte(V),length(V^)+1);
  27579. end;
  27580. CancelLastComma;
  27581. SetText(result);
  27582. finally
  27583. Free;
  27584. end;
  27585. end;
  27586. procedure TEnumType.GetEnumNameAll(var result: TRawUTF8DynArray;
  27587. TrimLeftLowerCase: boolean);
  27588. var max,i: integer;
  27589. V: PShortString;
  27590. begin
  27591. max := MaxValue-MinValue;
  27592. SetLength(result,max+1);
  27593. V := @NameList;
  27594. for i := 0 to max do begin
  27595. if TrimLeftLowerCase then
  27596. result[i] := TrimLeftLowerCaseShort(V) else
  27597. result[i] := RawUTF8(V^);
  27598. inc(PByte(V),length(V^)+1);
  27599. end;
  27600. end;
  27601. function TEnumType.GetEnumNameAllAsJSONArray(TrimLeftLowerCase: boolean): RawUTF8;
  27602. var i: integer;
  27603. V: PShortString;
  27604. begin
  27605. with TTextWriter.CreateOwnedStream(1024) do
  27606. try
  27607. Add('[');
  27608. V := @NameList;
  27609. for i := MinValue to MaxValue do begin
  27610. Add('"');
  27611. if TrimLeftLowerCase then
  27612. AddTrimLeftLowerCase(V) else
  27613. AddShort(V^);
  27614. Add('"',',');
  27615. inc(PByte(V),length(V^)+1);
  27616. end;
  27617. CancelLastComma;
  27618. Add(']');
  27619. SetText(result);
  27620. finally
  27621. Free;
  27622. end;
  27623. end;
  27624. procedure TEnumType.AddCaptionStrings(Strings: TStrings; UsedValuesBits: Pointer=nil);
  27625. var i, L: integer;
  27626. Line: array[byte] of AnsiChar;
  27627. P: PAnsiChar;
  27628. V: PShortString;
  27629. s: string;
  27630. begin
  27631. if @self=nil then
  27632. exit;
  27633. {$ifndef LVCL}
  27634. Strings.BeginUpdate;
  27635. try
  27636. {$endif}
  27637. V := @NameList;
  27638. for i := MinValue to MaxValue do begin
  27639. if (UsedValuesBits=nil) or
  27640. GetBit(UsedValuesBits^,i) then begin
  27641. L := ord(V^[0]);
  27642. P := @V^[1];
  27643. while (L>0) and (P^ in ['a'..'z']) do begin // ignore left lowercase chars
  27644. inc(P);
  27645. dec(L);
  27646. end;
  27647. if L=0 then begin
  27648. L := ord(V^[0]);
  27649. P := @V^[1];
  27650. end;
  27651. Line[L] := #0; // GetCaptionFromPCharLen() expect it as ASCIIZ
  27652. MoveFast(P^,Line,L);
  27653. GetCaptionFromPCharLen(Line,s);
  27654. Strings.AddObject(s,pointer(i));
  27655. end;
  27656. inc(PByte(V),length(V^)+1);
  27657. end;
  27658. {$ifndef LVCL}
  27659. finally
  27660. Strings.EndUpdate;
  27661. end;
  27662. {$endif}
  27663. end;
  27664. function TEnumType.GetCaptionStrings(UsedValuesBits: Pointer=nil): string;
  27665. var List: TStringList;
  27666. begin
  27667. List := TStringList.Create;
  27668. try
  27669. AddCaptionStrings(List,UsedValuesBits);
  27670. result := List.Text;
  27671. finally
  27672. List.Free;
  27673. end;
  27674. end;
  27675. function TEnumType.GetEnumNameTrimedValue(const EnumName: ShortString): Integer;
  27676. begin
  27677. result := FindShortStringListTrimLowerCase(@NameList,MaxValue,@EnumName[1],ord(EnumName[0]));
  27678. if result<0 then
  27679. result := FindShortStringListExact(@NameList,MaxValue,@EnumName[1],ord(EnumName[0]));
  27680. end;
  27681. function TEnumType.GetEnumNameTrimedValue(Value: PUTF8Char): Integer;
  27682. var ValueLen: integer;
  27683. begin
  27684. if Value=nil then
  27685. result := -1 else begin
  27686. ValueLen := StrLen(Value);
  27687. result := FindShortStringListTrimLowerCase(@NameList,MaxValue,Value,ValueLen);
  27688. if result<0 then
  27689. result := FindShortStringListExact(@NameList,MaxValue,Value,ValueLen);
  27690. end;
  27691. end;
  27692. function TEnumType.SizeInStorageAsEnum: Integer;
  27693. begin
  27694. case OrdType of // MaxValue does not work e.g. with WordBool
  27695. otSByte, otUByte: result := 1;
  27696. otSWord, otUWord: result := 2;
  27697. else result := 4;
  27698. end;
  27699. end;
  27700. procedure TEnumType.SetEnumFromOrdinal(out Value; Ordinal: Integer);
  27701. begin
  27702. case OrdType of // MaxValue does not work e.g. with WordBool
  27703. otSByte, otUByte: byte(Value) := Ordinal;
  27704. otSWord, otUWord: word(Value) := Ordinal;
  27705. else integer(Value) := Ordinal;
  27706. end;
  27707. end;
  27708. function TEnumType.SizeInStorageAsSet: Integer;
  27709. begin
  27710. case MaxValue of
  27711. 0..7: result := 1;
  27712. 8..15: result := 2;
  27713. 16..31: result := 4;
  27714. else result := 0;
  27715. end;
  27716. end;
  27717. function SQLWhereIsEndClause(const Where: RawUTF8): boolean;
  27718. begin
  27719. result := IdemPCharArray(pointer(Where),['ORDER BY ','GROUP BY ',
  27720. 'LIMIT ','OFFSET ','LEFT ','RIGHT ','INNER ','OUTER ','JOIN '])>=0;
  27721. end;
  27722. function SQLFromWhere(const Where: RawUTF8): RawUTF8;
  27723. begin
  27724. if Where='' then
  27725. result := '' else
  27726. if SQLWhereIsEndClause(Where) then
  27727. result := ' '+Where else
  27728. result := ' WHERE '+Where;
  27729. end;
  27730. function SQLFromSelect(const TableName, Select, Where, SimpleFields: RawUTF8): RawUTF8;
  27731. begin
  27732. if Select='*' then
  27733. // don't send BLOB values to query: retrieve all other fields
  27734. result := 'SELECT '+SimpleFields else
  27735. result := 'SELECT '+Select;
  27736. result := result+' FROM '+TableName+SQLFromWhere(Where);
  27737. end;
  27738. { TSQLRecordFill }
  27739. function TSQLRecordFill.GetJoinedFields: boolean;
  27740. begin
  27741. if self=nil then
  27742. result := false else
  27743. result := fJoinedFields;
  27744. end;
  27745. function TSQLRecordFill.TableMapFields: TSQLFieldBits;
  27746. begin
  27747. if self=nil then
  27748. FillZero(result) else
  27749. result := fTableMapFields;
  27750. end;
  27751. procedure TSQLRecordFill.AddMap(aRecord: TSQLRecord; aField: TSQLPropInfo;
  27752. aIndex: integer);
  27753. begin
  27754. if (self=nil) or (aRecord=nil) then
  27755. exit;
  27756. if fTableMapCount>=length(fTableMap) then
  27757. SetLength(fTableMap,fTableMapCount+fTableMapCount shr 1+16);
  27758. with fTableMap[fTableMapCount] do begin
  27759. Dest := aRecord;
  27760. DestField := aField;
  27761. TableIndex := aIndex;
  27762. inc(fTableMapCount);
  27763. end;
  27764. end;
  27765. procedure TSQLRecordFill.AddMap(aRecord: TSQLRecord; const aFieldName: RawUTF8;
  27766. aIndex: integer);
  27767. var aFieldIndex: integer;
  27768. begin
  27769. if (self<>nil) and (aRecord<>nil) then
  27770. if IsRowID(pointer(aFieldName)) then
  27771. AddMap(aRecord,nil,aIndex) else
  27772. with aRecord.RecordProps do begin
  27773. aFieldIndex := Fields.IndexByName(aFieldName);
  27774. if aFieldIndex>=0 then begin // only map if column name is a valid field
  27775. include(fTableMapFields,aFieldIndex);
  27776. AddMap(aRecord,Fields.List[aFieldIndex],aIndex);
  27777. end;
  27778. end;
  27779. end;
  27780. procedure TSQLRecordFill.AddMapSimpleFields(aRecord: TSQLRecord;
  27781. const aProps: array of TSQLPropInfo; var aIndex: integer);
  27782. var i: integer;
  27783. begin
  27784. AddMap(aRecord,nil,aIndex);
  27785. inc(aIndex);
  27786. for i := 0 to high(aProps) do
  27787. if aProps[i].SQLFieldTypeStored<>sftID then begin
  27788. AddMap(aRecord,aProps[i],aIndex);
  27789. inc(aIndex);
  27790. end;
  27791. end;
  27792. destructor TSQLRecordFill.Destroy;
  27793. begin
  27794. try
  27795. UnMap; // release fTable instance if necessary
  27796. finally
  27797. inherited;
  27798. end;
  27799. end;
  27800. function TSQLRecordFill.Fill(aRow: integer): Boolean;
  27801. begin
  27802. if (self=nil) or (Table=nil) or (cardinal(aRow)>cardinal(Table.fRowCount)) then
  27803. Result := False else begin
  27804. Fill(@Table.fResults[aRow*Table.FieldCount]);
  27805. Result := True;
  27806. end;
  27807. end;
  27808. function TSQLRecordFill.Fill(aRow: integer; aDest: TSQLRecord): Boolean;
  27809. begin
  27810. if (self=nil) or (aDest=nil) or
  27811. (Table=nil) or (cardinal(aRow)>cardinal(Table.fRowCount)) then
  27812. Result := False else begin
  27813. Fill(@Table.fResults[aRow*Table.FieldCount],aDest);
  27814. Result := True;
  27815. end;
  27816. end;
  27817. procedure TSQLRecordFill.Fill(aTableRow: PPUtf8CharArray);
  27818. var f: integer;
  27819. begin
  27820. if (self<>nil) and (aTableRow<>nil) then
  27821. for f := 0 to fTableMapCount-1 do
  27822. with fTableMap[f] do
  27823. if DestField=nil then
  27824. SetID(aTableRow[TableIndex],Dest.fID) else
  27825. DestField.SetValue(Dest,aTableRow[TableIndex],false);
  27826. end;
  27827. procedure TSQLRecordFill.Fill(aTableRow: PPUtf8CharArray; aDest: TSQLRecord);
  27828. var f: integer;
  27829. begin
  27830. if (self<>nil) and (aTableRow<>nil) then
  27831. for f := 0 to fTableMapCount-1 do
  27832. with fTableMap[f] do
  27833. if DestField=nil then
  27834. SetID(aTableRow[TableIndex],aDest.fID) else
  27835. DestField.SetValue(aDest,aTableRow[TableIndex],false);
  27836. end;
  27837. procedure TSQLRecordFill.ComputeSetUpdatedFieldBits(Props: TSQLRecordProperties;
  27838. out Bits: TSQLFieldBits);
  27839. begin
  27840. if (self<>nil) and (fTable<>nil) and (fTableMapRecordManyInstances=nil) then
  27841. // within FillPrepare/FillOne loop: update ID, TModTime and mapped fields
  27842. Bits := fTableMapFields+Props.ComputeBeforeUpdateFieldsBits else
  27843. // update all simple/custom fields (also for FillPrepareMany)
  27844. Bits := Props.SimpleFieldsBits[soUpdate];
  27845. end;
  27846. procedure TSQLRecordFill.Map(aRecord: TSQLRecord; aTable: TSQLTable;
  27847. aCheckTableName: TSQLCheckTableName);
  27848. var f: integer;
  27849. ColumnName: PUTF8Char;
  27850. FieldName: shortstring;
  27851. Props: TSQLRecordProperties;
  27852. begin
  27853. if aTable=nil then // avoid any GPF
  27854. exit;
  27855. fTable := aTable;
  27856. if aTable.fResults=nil then
  27857. exit; // void content
  27858. Props := aRecord.RecordProps;
  27859. for f := 0 to aTable.FieldCount-1 do begin
  27860. ColumnName := aTable.fResults[f];
  27861. if aCheckTableName=ctnNoCheck then
  27862. FieldName := ColumnName else
  27863. if IdemPChar(ColumnName,pointer(Props.SQLTableNameUpperWithDot)) then
  27864. FieldName := ColumnName+length(Props.SQLTableNameUpperWithDot) else
  27865. if aCheckTableName=ctnMustExist then
  27866. continue else
  27867. FieldName := ColumnName;
  27868. AddMap(aRecord,FieldName,f);
  27869. end;
  27870. fFillCurrentRow := 1; // point to first data row (0 is field names)
  27871. end;
  27872. procedure TSQLRecordFill.UnMap;
  27873. var i: integer;
  27874. begin
  27875. if self=nil then
  27876. exit;
  27877. fTableMapCount := 0;
  27878. fFillCurrentRow := 0;
  27879. // release TSQLRecordMany.fDestID^ instances set by TSQLRecord.FillPrepareMany()
  27880. for i := 0 to high(fTableMapRecordManyInstances) do
  27881. with fTableMapRecordManyInstances[i] do begin
  27882. TObject(fDestID^).Free;
  27883. fDestID^ := 0;
  27884. fSourceID^ := 0;
  27885. end;
  27886. fTableMapRecordManyInstances := nil;
  27887. FillZero(fTableMapFields);
  27888. // free any previous fTable if necessary
  27889. if Table<>nil then
  27890. try
  27891. if Table.OwnerMustFree then
  27892. Table.Free;
  27893. finally
  27894. fTable := nil;
  27895. end;
  27896. end;
  27897. { TSQLRecord }
  27898. constructor TSQLRecord.Create;
  27899. var i: integer;
  27900. begin
  27901. // auto-instanciate any TSQLRecordMany instance
  27902. with RecordProps do
  27903. if pointer(ManyFields)<>nil then
  27904. for i := 0 to high(ManyFields) do
  27905. ManyFields[i].SetInstance(self,TSQLRecordClass(ManyFields[i].ObjectClass).Create);
  27906. end;
  27907. constructor TSQLRecord.Create(const aSimpleFields: array of const; aID: TID);
  27908. begin
  27909. Create;
  27910. fID := aID;
  27911. if not SimplePropertiesFill(aSimpleFields) then
  27912. raise EORMException.CreateUTF8('Incorrect %.Create(aSimpleFields) call',[self]);
  27913. end;
  27914. function TSQLRecord.CreateCopy: TSQLRecord;
  27915. var f: integer;
  27916. begin
  27917. // create new instance
  27918. result := RecordClass.Create;
  27919. // copy properties content
  27920. result.fID := fID;
  27921. with RecordProps do
  27922. for f := 0 to high(CopiableFields) do
  27923. CopiableFields[f].CopyValue(self,result);
  27924. end;
  27925. function TSQLRecord.CreateCopy(const CustomFields: TSQLFieldBits): TSQLRecord;
  27926. var f: integer;
  27927. begin
  27928. result := RecordClass.Create;
  27929. // copy properties content
  27930. result.fID := fID;
  27931. with RecordProps do
  27932. for f := 0 to Fields.Count-1 do
  27933. with Fields.List[f] do
  27934. if (f in CustomFields) and (SQLFieldType in COPIABLE_FIELDS) then
  27935. CopyValue(self,result);
  27936. end;
  27937. constructor TSQLRecord.Create(aClient: TSQLRest; aID: TID; ForUpdate: boolean=false);
  27938. begin
  27939. Create;
  27940. if aClient<>nil then
  27941. aClient.Retrieve(aID,self,ForUpdate);
  27942. end;
  27943. constructor TSQLRecord.Create(aClient: TSQLRest; aPublishedRecord: TSQLRecord; ForUpdate: boolean);
  27944. begin
  27945. Create;
  27946. if aClient<>nil then
  27947. aClient.Retrieve(aPublishedRecord.ID,self,ForUpdate);
  27948. end;
  27949. constructor TSQLRecord.Create(aClient: TSQLRest; const aSQLWhere: RawUTF8);
  27950. begin
  27951. Create;
  27952. if aClient<>nil then
  27953. aClient.Retrieve(aSQLWhere,self);
  27954. end;
  27955. constructor TSQLRecord.Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  27956. const BoundsSQLWhere: array of const);
  27957. begin
  27958. Create;
  27959. if aClient<>nil then
  27960. aClient.Retrieve(FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere),self);
  27961. end;
  27962. constructor TSQLRecord.Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  27963. const ParamsSQLWhere, BoundsSQLWhere: array of const);
  27964. begin
  27965. Create;
  27966. if aClient<>nil then
  27967. aClient.Retrieve(FormatUTF8(FormatSQLWhere,ParamsSQLWhere,BoundsSQLWhere),self);
  27968. end;
  27969. constructor TSQLRecord.CreateFrom(const JSONRecord: RawUTF8);
  27970. begin
  27971. Create;
  27972. FillFrom(JSONRecord);
  27973. end;
  27974. constructor TSQLRecord.CreateFrom(P: PUTF8Char);
  27975. begin
  27976. Create;
  27977. FillFrom(P);
  27978. end;
  27979. {$ifndef NOVARIANTS}
  27980. constructor TSQLRecord.CreateFrom(const aDocVariant: variant);
  27981. begin
  27982. Create;
  27983. FillFrom(aDocVariant);
  27984. end;
  27985. {$endif}
  27986. class procedure TSQLRecord.InitializeTable(Server: TSQLRestServer;
  27987. const FieldName: RawUTF8; Options: TSQLInitializeTableOptions);
  27988. var f: integer;
  27989. begin // is not part of TSQLRecordProperties because has been declared as virtual
  27990. if (self<>nil) and (Server<>nil) and
  27991. (Options*INITIALIZETABLE_NOINDEX<>INITIALIZETABLE_NOINDEX) then begin
  27992. // ensure ID/RowID column is indexed
  27993. if not (itoNoIndex4ID in Options) then
  27994. if (FieldName='') or IsRowID(pointer(FieldName)) then
  27995. Server.CreateSQLIndex(self,'ID',true); // for external tables
  27996. // automatic column indexation of fields which are commonly searched by value
  27997. with RecordProps do
  27998. for f := 0 to Fields.Count-1 do
  27999. with Fields.List[f] do
  28000. if (FieldName='') or IdemPropNameU(FieldName,Name) then
  28001. if ((aIsUnique in Attributes) and not (itoNoIndex4UniqueField in Options)) or
  28002. ((SQLFieldType=sftRecord) and not (itoNoIndex4RecordReference in Options)) or
  28003. ((SQLFieldType=sftRecordVersion) and not (itoNoIndex4RecordVersion in Options)) or
  28004. ((SQLFieldType=sftID) and not (itoNoIndex4NestedRecord in Options)) or
  28005. ((SQLFieldType=sftTID) and not (itoNoIndex4TID in Options)) then
  28006. Server.CreateSQLIndex(self,Name,false);
  28007. end; // failure in Server.CreateSQLIndex() above is ignored (may already exist)
  28008. end;
  28009. procedure TSQLRecord.FillFrom(aRecord: TSQLRecord);
  28010. var i, f: integer;
  28011. S, D: TSQLRecordProperties;
  28012. SP: TSQLPropInfo;
  28013. wasString: boolean;
  28014. tmp: RawUTF8;
  28015. begin
  28016. if (self=nil) or (aRecord=nil) then
  28017. exit;
  28018. D := RecordProps;
  28019. if PSQLRecordClass(aRecord)^.InheritsFrom(PSQLRecordClass(self)^) then begin
  28020. if PSQLRecordClass(aRecord)^=PSQLRecordClass(self)^ then
  28021. fID := aRecord.fID; // same class -> ID values will match
  28022. for f := 0 to high(D.CopiableFields) do
  28023. D.CopiableFields[f].CopyValue(aRecord,self);
  28024. exit;
  28025. end;
  28026. S := aRecord.RecordProps; // two diverse tables -> don't copy ID
  28027. for i := 0 to high(S.CopiableFields) do begin
  28028. SP := S.CopiableFields[i];
  28029. if D.Fields.List[SP.PropertyIndex].Name=SP.Name then // optimistic match
  28030. f := SP.PropertyIndex else
  28031. f := D.Fields.IndexByName(S.CopiableFields[i].Name);
  28032. if f>=0 then begin
  28033. SP.GetValueVar(aRecord,False,tmp,@wasString);
  28034. D.Fields.List[f].SetValueVar(Self,tmp,wasString);
  28035. end;
  28036. end;
  28037. end;
  28038. procedure TSQLRecord.FillFrom(aRecord: TSQLRecord; const aRecordFieldBits: TSQLFieldBits);
  28039. var i, f: integer;
  28040. S, D: TSQLRecordProperties;
  28041. SP: TSQLPropInfo;
  28042. wasString: boolean;
  28043. tmp: RawUTF8;
  28044. begin
  28045. if (self=nil) or (aRecord=nil) then
  28046. exit;
  28047. D := RecordProps;
  28048. if PSQLRecordClass(aRecord)^.InheritsFrom(PSQLRecordClass(self)^) then begin
  28049. if PSQLRecordClass(aRecord)^=PSQLRecordClass(self)^ then
  28050. fID := aRecord.fID; // same class -> ID values will match
  28051. for f := 0 to D.Fields.Count-1 do
  28052. if f in aRecordFieldBits then
  28053. D.Fields.List[f].CopyValue(aRecord,self);
  28054. exit;
  28055. end;
  28056. S := aRecord.RecordProps; // two diverse tables -> don't copy ID
  28057. for i := 0 to S.Fields.Count-1 do
  28058. if i in aRecordFieldBits then begin
  28059. SP := S.Fields.List[i];
  28060. if D.Fields.List[i].Name=SP.Name then // optimistic match
  28061. f := i else
  28062. f := D.Fields.IndexByName(SP.Name);
  28063. if f>=0 then begin
  28064. SP.GetValueVar(aRecord,False,tmp,@wasString);
  28065. D.Fields.List[f].SetValueVar(Self,tmp,wasString);
  28066. end;
  28067. end;
  28068. end;
  28069. procedure TSQLRecord.FillFrom(Table: TSQLTable; Row: integer);
  28070. begin
  28071. try
  28072. FillPrepare(Table);
  28073. if Table.InternalState<>fInternalState then
  28074. fInternalState := Table.InternalState;
  28075. FillRow(Row);
  28076. finally
  28077. FillClose; // avoid GPF in TSQLRecord.Destroy
  28078. end;
  28079. end;
  28080. procedure TSQLRecord.FillFrom(const JSONTable: RawUTF8; Row: integer);
  28081. var Table: TSQLTableJSON;
  28082. tmp: TSynTempBuffer;
  28083. begin
  28084. tmp.Init(JSONTable);
  28085. Table := TSQLTableJSON.Create('',tmp.buf,tmp.len);
  28086. try
  28087. FillFrom(Table,Row);
  28088. finally
  28089. Table.Free;
  28090. tmp.Done;
  28091. end;
  28092. end;
  28093. procedure TSQLRecord.FillFrom(const JSONRecord: RawUTF8; FieldBits: PSQLFieldBits);
  28094. var tmp: TSynTempBuffer; // work on a private copy
  28095. begin
  28096. tmp.Init(JSONRecord);
  28097. try
  28098. FillFrom(tmp.buf,FieldBits); // now we can safely call FillFrom()
  28099. finally
  28100. tmp.Done;
  28101. end;
  28102. end;
  28103. procedure TSQLRecord.FillFrom(P: PUTF8Char; FieldBits: PSQLFieldBits);
  28104. (* two possible formats = first not expanded, 2nd is expanded (most useful)
  28105. {"fieldCount":9,"values":["ID","Int","Test","Unicode","Ansi","ValFloat","ValWord",
  28106. "ValDate","Next",0,0,"abcde+¬ef+á+¬","abcde+¬ef+á+¬","abcde+¬ef+á+¬",
  28107. 3.14159265300000E+0000,1203,"2009-03-10T21:19:36",0]}
  28108. {"ID":0,"Int":0,"Test":"abcde+¬ef+á+¬","Unicode":"abcde+¬ef+á+¬","Ansi":
  28109. "abcde+¬ef+á+¬","ValFloat": 3.14159265300000E+0000,"ValWord":1203,
  28110. "ValDate":"2009-03-10T21:19:36","Next":0} *)
  28111. var F: array[0..MAX_SQLFIELDS-1] of PUTF8Char; // store field/property names
  28112. wasString: boolean;
  28113. i, n: integer;
  28114. Prop, Value: PUTF8Char;
  28115. begin
  28116. if FieldBits<>nil then
  28117. FillZero(FieldBits^);
  28118. // go to start of object
  28119. if P=nil then
  28120. exit;
  28121. while P^<>'{' do
  28122. if P^=#0 then exit else inc(P);
  28123. if Expect(P,FIELDCOUNT_PATTERN) then begin
  28124. // not expanded format
  28125. n := GetJSONIntegerVar(P)-1;
  28126. if cardinal(n)>high(F) then
  28127. exit;
  28128. if Expect(P,ROWCOUNT_PATTERN) then
  28129. GetJSONIntegerVar(P); // just ignore "rowCount":.. here
  28130. if not Expect(P,VALUES_PATTERN) then
  28131. exit;
  28132. for i := 0 to n do
  28133. F[i] := GetJSONField(P,P);
  28134. for i := 0 to n do begin
  28135. Value := GetJSONFieldOrObjectOrArray(P,@wasString,nil,true);
  28136. FillValue(F[i],Value,wasString,FieldBits); // set properties from values
  28137. end;
  28138. end else
  28139. if P^='{' then begin
  28140. // expanded format
  28141. inc(P);
  28142. repeat
  28143. Prop := GetJSONPropName(P);
  28144. if (Prop=nil) or (P=nil) then break;
  28145. Value := GetJSONFieldOrObjectOrArray(P,@wasString,nil,true);
  28146. FillValue(Prop,Value,wasString,FieldBits); // set property from value
  28147. until P=nil;
  28148. end;
  28149. end;
  28150. {$ifndef NOVARIANTS}
  28151. procedure TSQLRecord.FillFrom(const aDocVariant: variant);
  28152. var json: RawUTF8;
  28153. begin
  28154. if _Safe(aDocVariant)^.Kind=dvObject then begin
  28155. VariantSaveJSON(aDocVariant,twJSONEscape, json);
  28156. FillFrom(pointer(json));
  28157. end;
  28158. end;
  28159. {$endif}
  28160. procedure TSQLRecord.FillPrepare(Table: TSQLTable; aCheckTableName: TSQLCheckTableName);
  28161. begin
  28162. if self=nil then
  28163. exit;
  28164. if fFill=nil then
  28165. fFill := TSQLRecordFill.Create else
  28166. fFill.UnMap;
  28167. fFill.Map(self,Table,aCheckTableName);
  28168. end;
  28169. function TSQLRecord.FillPrepare(aClient: TSQLRest; const aSQLWhere: RawUTF8;
  28170. const aCustomFieldsCSV: RawUTF8; aCheckTableName: TSQLCheckTableName): boolean;
  28171. var T: TSQLTable;
  28172. begin
  28173. result := false;
  28174. FillClose; // so that no further FillOne will work
  28175. if (self=nil) or (aClient=nil) then
  28176. exit;
  28177. T := aClient.MultiFieldValues(RecordClass,aCustomFieldsCSV,aSQLWhere);
  28178. if T=nil then
  28179. exit;
  28180. T.OwnerMustFree := true;
  28181. FillPrepare(T,aCheckTableName);
  28182. result := true;
  28183. end;
  28184. function TSQLRecord.FillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
  28185. const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): boolean;
  28186. begin
  28187. result := FillPrepare(aClient,FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere),
  28188. aCustomFieldsCSV);
  28189. end;
  28190. function TSQLRecord.FillPrepare(aClient: TSQLRest;
  28191. const FormatSQLWhere: RawUTF8; const ParamsSQLWhere, BoundsSQLWhere: array of const;
  28192. const aCustomFieldsCSV: RawUTF8): boolean;
  28193. begin
  28194. result := FillPrepare(aClient,
  28195. FormatUTF8(FormatSQLWhere,ParamsSQLWhere,BoundsSQLWhere),aCustomFieldsCSV);
  28196. end;
  28197. function TSQLRecord.FillPrepare(aClient: TSQLRest; const aIDs: array of Int64;
  28198. const aCustomFieldsCSV: RawUTF8=''): boolean;
  28199. begin
  28200. if high(aIDs)<0 then
  28201. result := false else
  28202. result := FillPrepare(aClient,
  28203. Int64DynArrayToCSV(aIDs,length(aIDs),'ID in (',')'),aCustomFieldsCSV);
  28204. end;
  28205. function TSQLRecord.FillRow(aRow: integer; aDest: TSQLRecord): boolean;
  28206. begin
  28207. if self<>nil then
  28208. if aDest=nil then
  28209. result := fFill.Fill(aRow) else
  28210. if fFill.fTableMapRecordManyInstances=nil then
  28211. result := fFill.Fill(aRow,aDest) else
  28212. raise EBusinessLayerException.CreateUTF8(
  28213. '%.FillRow() forbidden after FillPrepareMany',[self]) else
  28214. result := false;
  28215. end;
  28216. function TSQLRecord.FillOne: boolean;
  28217. begin
  28218. if (self=nil) or (fFill=nil) or (fFill.Table=nil) or
  28219. (fFill.Table.fRowCount=0) or // also check if FillTable is emtpy
  28220. (cardinal(fFill.FillCurrentRow)>cardinal(fFill.Table.fRowCount)) then
  28221. result := false else begin
  28222. FillRow(fFill.FillCurrentRow);
  28223. inc(fFill.fFillCurrentRow);
  28224. result := true;
  28225. end;
  28226. end;
  28227. function TSQLRecord.FillRewind: boolean;
  28228. begin
  28229. if (self=nil) or (fFill=nil) or (fFill.Table=nil) or (fFill.Table.fRowCount=0) then
  28230. result := false else begin
  28231. fFill.fFillCurrentRow := 1;
  28232. result := true;
  28233. end;
  28234. end;
  28235. procedure TSQLRecord.FillClose;
  28236. begin
  28237. if self<>nil then
  28238. fFill.UnMap;
  28239. end;
  28240. procedure TSQLRecord.AppendFillAsJsonValues(W: TJSONSerializer);
  28241. begin
  28242. W.Add('[');
  28243. while FillOne do begin
  28244. GetJSONValues(W);
  28245. W.Add(',');
  28246. end;
  28247. W.CancelLastComma;
  28248. W.Add(']');
  28249. end;
  28250. procedure TSQLRecord.FillValue(PropName: PUTF8Char; Value: PUTF8Char;
  28251. wasString: boolean; FieldBits: PSQLFieldBits);
  28252. var field: TSQLPropInfo;
  28253. begin
  28254. if self<>nil then
  28255. if IsRowID(PropName) then
  28256. SetID(Value,fID) else begin
  28257. field := RecordProps.Fields.ByName(PropName);
  28258. if field<>nil then begin
  28259. field.SetValue(self,Value,wasString);
  28260. if FieldBits<>nil then
  28261. Include(FieldBits^,field.PropertyIndex);
  28262. end;
  28263. end;
  28264. end;
  28265. function TSQLRecord.SetFieldSQLVars(const Values: TSQLVarDynArray): boolean;
  28266. var max, field: integer;
  28267. begin
  28268. result := false;
  28269. max := high(Values);
  28270. with RecordProps do begin
  28271. // expect exact Values[] type match with FieldType[]
  28272. if max<>Fields.Count-1 then // must match field count
  28273. exit else
  28274. for field := 0 to max do
  28275. if Fields.List[field].SQLDBFieldType<>Values[field].VType then
  28276. exit;
  28277. // now we can safely update field values
  28278. for field := 0 to max do
  28279. Fields.List[field].SetFieldSQLVar(self,Values[field]);
  28280. end;
  28281. result := true;
  28282. end;
  28283. procedure TSQLRecord.GetBinaryValues(W: TFileBufferWriter);
  28284. var f: integer;
  28285. begin
  28286. with RecordProps do
  28287. for f := 0 to Fields.Count-1 do
  28288. Fields.List[f].GetBinary(self,W);
  28289. end;
  28290. procedure TSQLRecord.GetBinaryValuesSimpleFields(W: TFileBufferWriter);
  28291. var f: integer;
  28292. begin
  28293. with RecordProps do
  28294. for f := 0 to SimpleFieldCount-1 do
  28295. SimpleFields[f].GetBinary(self,W);
  28296. end;
  28297. procedure TSQLRecord.GetBinaryValues(W: TFileBufferWriter;
  28298. const aFields: TSQLFieldBits);
  28299. var f: integer;
  28300. begin
  28301. with RecordProps do
  28302. for f := 0 to Fields.Count-1 do
  28303. if f in aFields then
  28304. Fields.List[f].GetBinary(self,W);
  28305. end;
  28306. function TSQLRecord.GetBinary: RawByteString;
  28307. var W: TFileBufferWriter;
  28308. begin
  28309. W := TFileBufferWriter.Create(TRawByteStringStream);
  28310. try
  28311. W.WriteVarUInt64(fID);
  28312. GetBinaryValues(W);
  28313. W.Flush;
  28314. result := (W.Stream as TRawByteStringStream).DataString;
  28315. finally
  28316. W.Free;
  28317. end;
  28318. end;
  28319. function TSQLRecord.SetBinary(P: PAnsiChar): Boolean;
  28320. begin
  28321. fID := FromVarUInt64(PByte(P));
  28322. result := SetBinaryValues(P);
  28323. end;
  28324. function TSQLRecord.SetBinaryValues(var P: PAnsiChar): boolean;
  28325. var f: integer;
  28326. begin
  28327. result := false;
  28328. with RecordProps do
  28329. for f := 0 to Fields.Count-1 do begin
  28330. P := Fields.List[f].SetBinary(self,P);
  28331. if P=nil then
  28332. exit; // on error
  28333. end;
  28334. result := true;
  28335. end;
  28336. function TSQLRecord.SetBinaryValuesSimpleFields(var P: PAnsiChar): Boolean;
  28337. var f: integer;
  28338. begin
  28339. result := false;
  28340. with RecordProps do
  28341. for f := 0 to SimpleFieldCount-1 do begin
  28342. P := SimpleFields[f].SetBinary(self,P);
  28343. if P=nil then
  28344. exit; // on error
  28345. end;
  28346. result := true;
  28347. end;
  28348. procedure TSQLRecord.GetJSONValues(W: TJSONSerializer);
  28349. var i,n: integer;
  28350. Props: TSQLPropInfoList;
  28351. begin
  28352. if self=nil then
  28353. exit;
  28354. // write the row data
  28355. if W.Expand then begin
  28356. W.Add('{');
  28357. if W.WithID then
  28358. W.AddString(W.ColNames[0]);
  28359. end;
  28360. if W.WithID then begin
  28361. W.Add(fID);
  28362. W.Add(',');
  28363. if (jwoID_str in W.fSQLRecordOptions) and W.Expand then begin
  28364. W.AddShort('"ID_str":"');
  28365. W.Add(fID);
  28366. W.Add('"',',');
  28367. end;
  28368. n := 1;
  28369. end else
  28370. n := 0;
  28371. if W.Fields<>nil then begin
  28372. Props := RecordProps.Fields;
  28373. for i := 0 to length(W.Fields)-1 do begin
  28374. if W.Expand then begin
  28375. W.AddString(W.ColNames[n]); // '"'+ColNames[]+'":'
  28376. inc(n);
  28377. end;
  28378. Props.List[W.Fields[i]].GetJSONValues(Self,W);
  28379. W.Add(',');
  28380. end;
  28381. end;
  28382. W.CancelLastComma; // cancel last ','
  28383. if W.Expand then
  28384. W.Add('}');
  28385. end;
  28386. procedure TSQLRecord.AppendAsJsonObject(W: TJSONSerializer; Fields: TSQLFieldBits);
  28387. var i: integer;
  28388. Props: TSQLPropInfoList;
  28389. begin
  28390. if Self=nil then begin
  28391. W.AddShort('null');
  28392. exit;
  28393. end;
  28394. W.AddShort('{"ID":');
  28395. W.Add(fID);
  28396. if IsZero(Fields) then
  28397. Fields := RecordProps.SimpleFieldsBits[soSelect];
  28398. Props := RecordProps.Fields;
  28399. for i := 0 to Props.Count-1 do
  28400. if i in Fields then begin
  28401. W.Add(',','"');
  28402. W.AddNoJSONEscape(pointer(Props.List[i].Name),length(Props.List[i].Name));
  28403. W.Add('"',':');
  28404. Props.List[i].GetJSONValues(Self,W);
  28405. end;
  28406. W.Add('}');
  28407. end;
  28408. procedure TSQLRecord.AppendFillAsJsonArray(const FieldName: RawUTF8;
  28409. W: TJSONSerializer; Fields: TSQLFieldBits=[]);
  28410. begin
  28411. if FieldName<>'' then
  28412. W.AddFieldName(FieldName);
  28413. W.Add('[');
  28414. while FillOne do begin
  28415. AppendAsJsonObject(W,Fields);
  28416. W.Add(',');
  28417. end;
  28418. W.CancelLastComma;
  28419. W.Add(']');
  28420. if FieldName<>'' then
  28421. W.Add(',');
  28422. end;
  28423. procedure TSQLRecord.ForceVariantFieldsOptions(aOptions: TDocVariantOptions);
  28424. var i: integer;
  28425. begin
  28426. if self<>nil then
  28427. with RecordProps do
  28428. if sftVariant in HasTypeFields then
  28429. for i := 0 to Fields.Count-1 do
  28430. with TSQLPropInfoRTTIVariant(Fields.List[i]) do
  28431. if (SQLFieldType=sftVariant) and InheritsFrom(TSQLPropInfoRTTIVariant) then
  28432. if PropInfo.GetterIsField then
  28433. with _Safe(PVariant(PropInfo.GetterAddr(self))^)^ do
  28434. if Count>0 then
  28435. Options := aOptions;
  28436. end;
  28437. procedure TSQLRecord.GetJSONValuesAndFree(JSON : TJSONSerializer);
  28438. begin
  28439. if JSON<>nil then
  28440. try
  28441. // write the row data
  28442. GetJSONValues(JSON);
  28443. // end the JSON object
  28444. if not JSON.Expand then
  28445. JSON.AddNoJSONEscape(PAnsiChar(']}'),2);
  28446. JSON.FlushFinal;
  28447. finally
  28448. JSON.Free;
  28449. end;
  28450. end;
  28451. procedure TSQLRecord.GetJSONValues(JSON: TStream; Expand: boolean; withID: boolean;
  28452. Occasion: TSQLOccasion; SQLRecordOptions: TJSONSerializerSQLRecordOptions);
  28453. var serializer: TJSONSerializer;
  28454. begin
  28455. if self=nil then
  28456. exit;
  28457. with RecordProps do
  28458. serializer := CreateJSONWriter(JSON,Expand,withID,SimpleFieldsBits[Occasion],0);
  28459. serializer.SQLRecordOptions := SQLRecordOptions;
  28460. GetJSONValuesAndFree(serializer);
  28461. end;
  28462. function TSQLRecord.GetJSONValues(Expand: boolean; withID: boolean;
  28463. const Fields: TSQLFieldBits; SQLRecordOptions: TJSONSerializerSQLRecordOptions): RawUTF8;
  28464. var J: TRawByteStringStream;
  28465. serializer: TJSONSerializer;
  28466. begin
  28467. J := TRawByteStringStream.Create;
  28468. try
  28469. serializer := RecordProps.CreateJSONWriter(J,Expand,withID,Fields,0);
  28470. serializer.SQLRecordOptions := SQLRecordOptions;
  28471. GetJSONValuesAndFree(serializer);
  28472. result := J.DataString;
  28473. finally
  28474. J.Free;
  28475. end;
  28476. end;
  28477. function TSQLRecord.GetJSONValues(Expand: boolean; withID: boolean;
  28478. const FieldsCSV: RawUTF8; SQLRecordOptions: TJSONSerializerSQLRecordOptions): RawUTF8;
  28479. var bits: TSQLFieldBits;
  28480. begin
  28481. if RecordProps.FieldBitsFromCSV(FieldsCSV,bits) then
  28482. result := GetJSONValues(Expand,withID,bits,SQLRecordOptions) else
  28483. result := '';
  28484. end;
  28485. function TSQLRecord.GetJSONValues(Expand: boolean; withID: boolean;
  28486. Occasion: TSQLOccasion; UsingStream: TCustomMemoryStream;
  28487. SQLRecordOptions: TJSONSerializerSQLRecordOptions): RawUTF8;
  28488. var J: TRawByteStringStream;
  28489. begin
  28490. if (not withID) and IsZero(RecordProps.SimpleFieldsBits[Occasion]) then
  28491. // no simple field to write -> quick return
  28492. result := '' else
  28493. if UsingStream<>nil then begin
  28494. UsingStream.Seek(0,soFromBeginning);
  28495. GetJSONValues(UsingStream,Expand,withID,Occasion,SQLRecordOptions);
  28496. SetString(result,PAnsiChar(UsingStream.Memory),UsingStream.Seek(0,soFromCurrent));
  28497. end else begin
  28498. J := TRawByteStringStream.Create;
  28499. try
  28500. GetJSONValues(J,Expand,withID,Occasion,SQLRecordOptions);
  28501. result := J.DataString;
  28502. finally
  28503. J.Free;
  28504. end;
  28505. end;
  28506. end;
  28507. function GetVirtualTableSQLCreate(Props: TSQLRecordProperties): RawUTF8;
  28508. var i: integer;
  28509. SQL: RawUTF8;
  28510. begin
  28511. result := ''; // RowID is added by sqlite3_declare_vtab() for a Virtual Table
  28512. for i := 0 to Props.Fields.Count-1 do
  28513. with Props.Fields.List[i] do begin
  28514. SQL := Props.SQLFieldTypeToSQL(i); // = '' for field with no matching DB column
  28515. if SQL<>'' then
  28516. result := result+Name+SQL;
  28517. end;
  28518. if result='' then
  28519. result := ');' else
  28520. pWord(@result[length(result)-1])^ := ord(')')+ord(';')shl 8;
  28521. end;
  28522. class function TSQLRecord.GetSQLCreate(aModel: TSQLModel): RawUTF8;
  28523. // not implemented in TSQLRecordProperties since has been made virtual
  28524. var i: integer;
  28525. SQL: RawUTF8;
  28526. M: TSQLVirtualTableClass;
  28527. Props: TSQLModelRecordProperties;
  28528. begin
  28529. if aModel=nil then
  28530. raise EModelException.CreateUTF8('Invalid %.GetSQLCreate(nil) call',[self]);
  28531. Props := aModel.Props[self];
  28532. if Props.Kind<>rSQLite3 then begin
  28533. // create a FTS3/FTS4/RTREE virtual table
  28534. result := 'CREATE VIRTUAL TABLE '+SQLTableName+' USING ';
  28535. case Props.Kind of
  28536. rFTS3:
  28537. result := result+'fts3(';
  28538. rFTS4:
  28539. result := result+'fts4(';
  28540. rRTree: result := result+'rtree(RowID,';
  28541. rCustomForcedID, rCustomAutoID: begin
  28542. M := aModel.VirtualTableModule(self);
  28543. if M=nil then
  28544. raise EModelException.CreateUTF8('No registered module for %',[self]);
  28545. if Props.Props.Fields.Count=0 then
  28546. raise EModelException.CreateUTF8(
  28547. 'Virtual % class % should have published properties',[M.ModuleName,self]);
  28548. result := result+M.ModuleName+'(';
  28549. result := result+GetVirtualTableSQLCreate(Props.Props);
  28550. end;
  28551. end;
  28552. with Props.Props.Fields do
  28553. case Props.Kind of
  28554. rFTS3, rFTS4: begin
  28555. if (Props.fFTSWithoutContentFields<>'') and (Props.fFTSWithoutContentTableIndex>=0) then
  28556. result := result+'content="'+aModel.Tables[Props.fFTSWithoutContentTableIndex].
  28557. SQLTableName+'",';
  28558. if Count=0 then
  28559. raise EModelException.CreateUTF8(
  28560. 'Virtual FTS class % should have published properties',[self]);
  28561. for i := 0 to Count-1 do
  28562. with List[i] do
  28563. if SQLFieldTypeStored<>sftUTF8Text then
  28564. raise EModelException.CreateUTF8('%.%: FTS3/FTS4 field must be RawUTF8',
  28565. [self,Name]) else
  28566. result := result+Name+',';
  28567. if InheritsFrom(TSQLRecordFTS3Porter) or
  28568. InheritsFrom(TSQLRecordFTS4Porter) then
  28569. result := result+' tokenize=porter)' else
  28570. if InheritsFrom(TSQLRecordFTS3Unicode61) or
  28571. InheritsFrom(TSQLRecordFTS4Unicode61) then
  28572. result := result+' tokenize=unicode61)' else
  28573. result := result+' tokenize=simple)';
  28574. end;
  28575. rRTree: begin
  28576. if (Count<2) or (Count>RTREE_MAX_DIMENSION*2) or
  28577. (Count and 2<>0) then
  28578. raise EModelException.CreateUTF8('% has % fields: RTREE expects 2,4,6..%',
  28579. [self,Count,RTREE_MAX_DIMENSION*2]);
  28580. for i := 0 to Count-1 do
  28581. with List[i] do
  28582. if SQLFieldTypeStored<>sftFloat then
  28583. raise EModelException.CreateUTF8('%.%: RTREE field must be double',[self,Name]) else
  28584. result := result+Name+',';
  28585. result[length(result)] := ')';
  28586. end;
  28587. end;
  28588. end else begin
  28589. // inherits from TSQLRecord: create a "normal" SQLite3 table
  28590. result := 'CREATE TABLE '+SQLTableName+
  28591. '(ID INTEGER PRIMARY KEY AUTOINCREMENT, ';
  28592. // we always add an ID field which is an INTEGER PRIMARY KEY
  28593. // column, as it is always created (as hidden ROWID) by the SQLite3 engine
  28594. with Props.Props do
  28595. for i := 0 to Fields.Count-1 do
  28596. with Fields.List[i] do begin
  28597. SQL := SQLFieldTypeToSQL(i); // = '' for field with no matching DB column
  28598. if SQL<>'' then begin
  28599. result := result+Name+SQL;
  28600. if i in IsUniqueFieldsBits then
  28601. insert(' UNIQUE',result,length(result)-1);
  28602. end;
  28603. end;
  28604. pWord(@result[length(result)-1])^ := ord(')')+ord(';')shl 8;
  28605. end;
  28606. end;
  28607. function TSQLRecord.GetSQLSet: RawUTF8;
  28608. var i: integer;
  28609. V: RawUTF8;
  28610. wasString: boolean;
  28611. begin
  28612. result := '';
  28613. if self=nil then
  28614. exit;
  28615. with RecordProps do
  28616. for i := 0 to High(SimpleFields) do
  28617. with SimpleFields[i] do begin
  28618. // format is 'COL1='VAL1', COL2='VAL2'' }
  28619. GetValueVar(self,true,V,@wasString);
  28620. if wasString then
  28621. V := QuotedStr(V);
  28622. result := result+Name+'='+V+', ';
  28623. end;
  28624. if result<>'' then
  28625. SetLength(result,length(result)-2);
  28626. end;
  28627. function TSQLRecord.GetSQLValues: RawUTF8;
  28628. var i: integer;
  28629. V: RawUTF8;
  28630. wasString: boolean;
  28631. begin
  28632. result := '';
  28633. if self<>nil then
  28634. with RecordProps do
  28635. if SimpleFields=nil then
  28636. exit else begin
  28637. if HasNotSimpleFields then // get 'COL1,COL2': no 'ID,' for INSERT (false below)
  28638. result := SQLTableSimpleFieldsNoRowID; // always <> '*'
  28639. result := result+' VALUES (';
  28640. for i := 0 to high(SimpleFields) do
  28641. with SimpleFields[i] do begin
  28642. GetValueVar(self,true,V,@wasString);
  28643. if wasString then
  28644. V := QuotedStr(V);
  28645. result := result+V+',';
  28646. end;
  28647. result[length(result)] := ')';
  28648. end;
  28649. end;
  28650. class function TSQLRecord.CaptionName(Action: PRawUTF8=nil; ForHint: boolean=false): string;
  28651. begin
  28652. if Action=nil then
  28653. GetCaptionFromPCharLen(pointer(RecordProps.SQLTableName),result) else
  28654. GetCaptionFromPCharLen(TrimLeftLowerCase(Action^),result);
  28655. end;
  28656. class function TSQLRecord.CaptionNameFromRTTI(Action: PShortString): string;
  28657. var tmp: RawUTF8;
  28658. begin
  28659. if Action=nil then
  28660. result := CaptionName(nil) else begin
  28661. SetString(tmp,PAnsiChar(@Action^[1]),ord(Action^[0]));
  28662. result := CaptionName(@tmp);
  28663. end;
  28664. end;
  28665. function TSQLRecord.SameValues(Reference: TSQLRecord): boolean;
  28666. var O: TSQLPropInfo;
  28667. i: integer;
  28668. This,Ref: TSQLRecordProperties;
  28669. begin
  28670. result := false;
  28671. if (self=nil) or (Reference=nil) or
  28672. (Reference.fID<>fID) then // ID field must be tested by hand
  28673. exit;
  28674. if self<>Reference then
  28675. if (PSQLRecordClass(Reference)^=PSQLRecordClass(self)^) then begin
  28676. // faster comparison on same exact class
  28677. with RecordProps do
  28678. for i := 0 to high(SimpleFields) do
  28679. // compare not TSQLRawBlob/TSQLRecordMany fields
  28680. with SimpleFields[i] do
  28681. if CompareValue(self,Reference,false)<>0 then
  28682. exit; // properties don't have the same value
  28683. end else begin
  28684. // comparaison of all properties of Reference against self
  28685. This := RecordProps;
  28686. Ref := Reference.RecordProps;
  28687. for i := 0 to high(Ref.SimpleFields) do
  28688. with Ref.SimpleFields[i] do begin
  28689. // compare not TSQLRawBlob/TSQLRecordMany fields
  28690. O := This.Fields.ByRawUTF8Name(Name);
  28691. if O=nil then
  28692. exit; // this Reference property doesn't exist in current object
  28693. if GetValue(Reference,false,nil)<>O.GetValue(self,false,nil) then
  28694. exit; // properties don't have the same value
  28695. end;
  28696. end;
  28697. result := true;
  28698. end;
  28699. function TSQLRecord.SameRecord(Reference: TSQLRecord): boolean;
  28700. var i: integer;
  28701. begin
  28702. result := false;
  28703. if (self=nil) or (Reference=nil) or
  28704. (PSQLRecordClass(Reference)^<>PSQLRecordClass(Self)^) or (Reference.fID<>fID) then
  28705. exit;
  28706. with RecordProps do
  28707. for i := 0 to high(SimpleFields) do
  28708. // compare not TSQLRawBlob/TSQLRecordMany fields
  28709. with SimpleFields[i] do
  28710. if CompareValue(self,Reference,false)<>0 then
  28711. exit; // properties don't have the same value
  28712. result := true;
  28713. end;
  28714. procedure TSQLRecord.ClearProperties;
  28715. var i: integer;
  28716. begin
  28717. if self=nil then
  28718. exit;
  28719. fInternalState := 0;
  28720. fID := 0;
  28721. with RecordProps do
  28722. if fFill.JoinedFields then begin
  28723. for i := 0 to high(CopiableFields) do
  28724. if CopiableFields[i].SQLFieldType<>sftID then
  28725. CopiableFields[i].SetValue(self,nil,false) else
  28726. TSQLRecord(TSQLPropInfoRTTIInstance(CopiableFields[i]).GetInstance(Self)).
  28727. ClearProperties; // clear nested allocated TSQLRecord
  28728. end else
  28729. for i := 0 to high(CopiableFields) do
  28730. CopiableFields[i].SetValue(self,nil,false);
  28731. end;
  28732. procedure TSQLRecord.ClearProperties(const aFieldsCSV: RawUTF8);
  28733. var bits: TSQLFieldBits;
  28734. f: integer;
  28735. begin
  28736. if (self=nil) or (aFieldsCSV='') then
  28737. exit;
  28738. with RecordProps do begin
  28739. if aFieldsCSV='*' then
  28740. bits := SimpleFieldsBits[soInsert] else
  28741. if not FieldBitsFromCSV(aFieldsCSV,bits) then
  28742. exit;
  28743. for f := 0 to Fields.Count-1 do
  28744. if (f in bits) and (Fields.List[f].SQLFieldType in COPIABLE_FIELDS) then
  28745. Fields.List[f].SetValue(self,nil,false); // clear field value
  28746. end;
  28747. end;
  28748. {$IFDEF PUREPASCAL}
  28749. function TSQLRecord.RecordClass: TSQLRecordClass;
  28750. begin
  28751. if self=nil then
  28752. Result := nil else
  28753. Result := PSQLRecordClass(Self)^;
  28754. end;
  28755. {$else}
  28756. function TSQLRecord.RecordClass: TSQLRecordClass;
  28757. asm
  28758. test eax,eax; jz @z
  28759. mov eax,[eax]
  28760. @z:
  28761. end;
  28762. {$endif}
  28763. {$IFDEF PUREPASCAL}
  28764. function TSQLRecord.ClassProp: PClassProp;
  28765. begin
  28766. if self<>nil then
  28767. result := InternalClassProp(ClassType) else
  28768. result := nil; // avoid GPF
  28769. end;
  28770. {$else}
  28771. function TSQLRecord.ClassProp: PClassProp;
  28772. asm
  28773. test eax,eax; jz @z // avoid GPF
  28774. mov eax,[eax] // get ClassType of this TSQLRecord instance
  28775. test eax,eax; jz @z // avoid GPF
  28776. mov eax,[eax+vmtTypeInfo]
  28777. test eax,eax; jz @z // avoid GPF
  28778. movzx edx,byte ptr [eax].TTypeInfo.Name
  28779. lea eax,[eax+edx].TTypeInfo.Name[1]
  28780. movzx edx,byte ptr [eax].TClassType.UnitName
  28781. lea eax,[eax+edx].TClassType.UnitName[1].TClassProp
  28782. @z:
  28783. end;
  28784. {$endif}
  28785. function TSQLRecord.RecordReference(Model: TSQLModel): TRecordReference;
  28786. begin
  28787. if (self=nil) or (fID<=0) then
  28788. result := 0 else begin
  28789. result := Model.GetTableIndexExisting(PSQLRecordClass(Self)^);
  28790. if result>63 then // TRecordReference handle up to 64=1 shl 6 tables
  28791. result := 0 else
  28792. inc(result,fID shl 6);
  28793. end;
  28794. end;
  28795. destructor TSQLRecord.Destroy;
  28796. var i: integer;
  28797. props: TSQLRecordProperties;
  28798. begin
  28799. props := RecordProps;
  28800. if fFill<>nil then begin
  28801. if fFill.fJoinedFields then
  28802. // free all TSQLRecord instances created by TSQLRecord.CreateJoined
  28803. for i := 0 to length(props.JoinedFields)-1 do
  28804. props.JoinedFields[i].GetInstance(self).Free;
  28805. fFill.Free; // call UnMap -> release fTable instance if necessary
  28806. end;
  28807. // free all TSQLRecordMany instances created by TSQLRecord.Create
  28808. if props.ManyFields<>nil then
  28809. for i := 0 to length(props.ManyFields)-1 do
  28810. props.ManyFields[i].GetInstance(self).Free;
  28811. // free any registered T*ObjArray
  28812. if props.DynArrayFieldsHasObjArray then
  28813. for i := 0 to length(props.DynArrayFields)-1 do
  28814. with props.DynArrayFields[i] do
  28815. if ObjArray<>nil then
  28816. ObjArrayClear(fPropInfo^.GetFieldAddr(self)^);
  28817. inherited;
  28818. end;
  28819. function TSQLRecord.SimplePropertiesFill(const aSimpleFields: array of const): boolean;
  28820. var i: integer;
  28821. tmp: RawUTF8;
  28822. begin
  28823. if self=nil then
  28824. result := false else // means error
  28825. with RecordProps do
  28826. if length(SimpleFields)<>length(aSimpleFields) then
  28827. result := false else begin
  28828. for i := 0 to high(aSimpleFields) do begin
  28829. VarRecToUTF8(aSimpleFields[i],tmp); // will work for every handled type
  28830. SimpleFields[i].SetValueVar(self,tmp,false);
  28831. end;
  28832. result := True;
  28833. end;
  28834. end;
  28835. constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest;
  28836. const aSQLWhere: RawUTF8; const aCustomFieldsCSV: RawUTF8='');
  28837. var aTable: TSQLTable;
  28838. begin
  28839. Create;
  28840. aTable := aClient.MultiFieldValues(RecordClass,aCustomFieldsCSV,aSQLWhere);
  28841. if aTable=nil then
  28842. exit;
  28843. aTable.OwnerMustFree := true;
  28844. FillPrepare(aTable);
  28845. end;
  28846. constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest;
  28847. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  28848. const aCustomFieldsCSV: RawUTF8='');
  28849. var where: RawUTF8;
  28850. begin
  28851. where := FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere);
  28852. CreateAndFillPrepare(aClient,where,aCustomFieldsCSV);
  28853. end;
  28854. constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest;
  28855. const FormatSQLWhere: RawUTF8; const ParamsSQLWhere,
  28856. BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8);
  28857. var where: RawUTF8;
  28858. begin
  28859. where := FormatUTF8(FormatSQLWhere,ParamsSQLWhere,BoundsSQLWhere);
  28860. CreateAndFillPrepare(aClient,where,aCustomFieldsCSV);
  28861. end;
  28862. constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest;
  28863. const aIDs: array of Int64; const aCustomFieldsCSV: RawUTF8='');
  28864. begin
  28865. Create;
  28866. FillPrepare(aClient,aIDs,aCustomFieldsCSV);
  28867. end;
  28868. constructor TSQLRecord.CreateAndFillPrepare(const aJSON: RawUTF8);
  28869. var aTable: TSQLTable;
  28870. begin
  28871. Create;
  28872. aTable := TSQLTableJSON.CreateFromTables([RecordClass],'',aJSON);
  28873. aTable.OwnerMustFree := true;
  28874. FillPrepare(aTable);
  28875. end;
  28876. constructor TSQLRecord.CreateAndFillPrepareJoined(aClient: TSQLRest;
  28877. const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const);
  28878. var i,n: integer;
  28879. props: TSQLModelRecordProperties;
  28880. T: TSQLTableJSON;
  28881. instance: TSQLRecord;
  28882. SQL: RawUTF8;
  28883. begin
  28884. Create;
  28885. props := aClient.Model.Props[PSQLRecordClass(Self)^];
  28886. if props.props.JoinedFields=nil then
  28887. raise EORMException.CreateUTF8('No nested TSQLRecord to JOIN in %',[self]);
  28888. SQL := props.SQL.SelectAllJoined;
  28889. if aFormatSQLJoin<>'' then
  28890. SQL := SQL+FormatUTF8(SQLFromWhere(aFormatSQLJoin),aParamsSQLJoin,aBoundsSQLJoin);
  28891. T := aClient.ExecuteList(props.props.JoinedFieldsTable,SQL);
  28892. if T=nil then
  28893. exit;
  28894. fFill := TSQLRecordFill.Create;
  28895. fFill.fJoinedFields := True;
  28896. fFill.fTable := T;
  28897. fFill.fTable.OwnerMustFree := true;
  28898. n := 0;
  28899. with props.props do begin // follow SQL.SelectAllJoined columns
  28900. fFill.AddMapSimpleFields(Self,SimpleFields,n);
  28901. for i := 1 to high(JoinedFieldsTable) do begin
  28902. instance := JoinedFieldsTable[i].Create;
  28903. JoinedFields[i-1].SetInstance(self,instance);
  28904. fFill.AddMapSimpleFields(instance,JoinedFieldsTable[i].RecordProps.SimpleFields,n);
  28905. end;
  28906. end;
  28907. fFill.fFillCurrentRow := 1; // point to first data row (0 is field names)
  28908. end;
  28909. constructor TSQLRecord.CreateJoined(aClient: TSQLRest; aID: TID);
  28910. begin
  28911. CreateAndFillPrepareJoined(aClient,'%.RowID=?',[RecordProps.SQLTableName],[aID]);
  28912. FillOne;
  28913. end;
  28914. constructor TSQLRecord.CreateAndFillPrepareMany(aClient: TSQLRest;
  28915. const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const);
  28916. begin
  28917. Create;
  28918. if Length(RecordProps.ManyFields)=0 then
  28919. raise EModelException.CreateUTF8(
  28920. '%.CreateAndFillPrepareMany() with no many-to-many fields',[self]);
  28921. if not FillPrepareMany(aClient,aFormatSQLJoin,aParamsSQLJoin,aBoundsSQLJoin) then
  28922. raise EModelException.CreateUTF8(
  28923. '%.CreateAndFillPrepareMany(): FillPrepareMany() failure',[self]);
  28924. end;
  28925. function TSQLRecord.EnginePrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8;
  28926. const aParamsSQLJoin, aBoundsSQLJoin: array of const;
  28927. out ObjectsClass: TSQLRecordClassDynArray; out SQL: RawUTF8): RawUTF8;
  28928. var aSQLFields, aSQLFrom, aSQLWhere, aSQLJoin: RawUTF8;
  28929. aField: string[3];
  28930. aMany: RawUTF8;
  28931. f, n, i, SQLFieldsCount: Integer;
  28932. Props: TSQLRecordProperties;
  28933. SQLFields: array of record
  28934. SQL: string[3];
  28935. Prop: TSQLPropInfo;
  28936. Instance: TSQLRecord;
  28937. end;
  28938. M: TSQLRecordMany;
  28939. D: TSQLRecord;
  28940. J,JBeg: PUTF8Char;
  28941. Objects: array of TSQLRecord;
  28942. function AddField(aProp: TSQLPropInfo): Boolean;
  28943. begin
  28944. if SQLFieldsCount>=MAX_SQLFIELDS then
  28945. result := false else
  28946. with SQLFields[SQLFieldsCount] do begin
  28947. SQL := aField;
  28948. Prop := aProp;
  28949. Instance := Objects[f];
  28950. inc(SQLFieldsCount);
  28951. result := true;
  28952. end;
  28953. end;
  28954. function ProcessField(var P: PUTF8Char): RawUTF8;
  28955. var B: PUTF8Char;
  28956. field: TSQLPropInfo;
  28957. i: integer;
  28958. M: TSQLRecordMany;
  28959. aManyField: string[63];
  28960. function GetManyField(F: PUTF8Char): boolean;
  28961. var B: PUTF8Char;
  28962. begin
  28963. result := true;
  28964. B := F;
  28965. while ord(F^) in IsIdentifier do inc(F); // go to end of sub-field name
  28966. if B=F then begin
  28967. result := false;
  28968. exit;
  28969. end;
  28970. dec(B,2); // space for 'C.'
  28971. SetString(aManyField,B,F-B);
  28972. aManyField[2] := '.';
  28973. P := F;
  28974. end;
  28975. begin
  28976. B := P;
  28977. while ord(P^) in IsIdentifier do inc(P); // go to end of field name
  28978. SetString(result,B,P-B);
  28979. if (result='') or IdemPropNameU(result,'AND') or IdemPropNameU(result,'OR') or
  28980. IdemPropNameU(result,'LIKE') or IdemPropNameU(result,'NOT') or
  28981. IdemPropNameU(result,'NULL') then
  28982. exit;
  28983. if not IsRowID(pointer(result)) then begin
  28984. i := Props.Fields.IndexByName(result);
  28985. if i<0 then
  28986. exit;
  28987. field := Props.Fields.List[i];
  28988. if field.SQLFieldType=sftMany then begin
  28989. M := TSQLPropInfoRTTIInstance(field).GetInstance(self) as TSQLRecordMany;
  28990. for i := 0 to n-1 do
  28991. if Objects[i*2+1]=M then begin
  28992. if IdemPChar(P,'.DEST.') then begin // special case of Many.Dest.*
  28993. if GetManyField(P+6) then begin
  28994. aManyField[1] := AnsiChar(i*2+67);
  28995. result := RawUTF8(aManyField);
  28996. exit; // Categories.Dest.Name=? -> C.Name=?
  28997. end;
  28998. end else
  28999. if (P^='.') and GetManyField(P+1) then begin
  29000. aManyField[1] := AnsiChar(i*2+66);
  29001. result := RawUTF8(aManyField);
  29002. exit; // Categories.Kind=? -> CC.Kind=?
  29003. end;
  29004. end;
  29005. exit;
  29006. end;
  29007. end;
  29008. result := 'A.'+result; // Owner=? -> A.Owner=?
  29009. end;
  29010. begin
  29011. result := '';
  29012. FillClose; // so that no further FillOne will work
  29013. if (self=nil) or (aClient=nil) then
  29014. exit;
  29015. // reset TSQLRecordFill object
  29016. if fFill=nil then
  29017. fFill := TSQLRecordFill.Create else
  29018. fFill.UnMap;
  29019. // compute generic joined SQL statement and initialize Objects*[]+SQLFields[]
  29020. SetLength(SQLFields,MAX_SQLFIELDS);
  29021. Props := RecordProps;
  29022. n := Length(Props.ManyFields);
  29023. if n=0 then
  29024. exit;
  29025. SetLength(Objects,n*2+1);
  29026. SetLength(ObjectsClass,n*2+1);
  29027. Objects[0] := self;
  29028. ObjectsClass[0] := PSQLRecordClass(self)^;
  29029. SetLength(fFill.fTableMapRecordManyInstances,n); // fFill.UnMap will release memory
  29030. aSQLWhere := ''; // alf: to circumvent FPC issues
  29031. aSQLFields := '';
  29032. aSQLFrom := '';
  29033. for f := 0 to n-1 do begin
  29034. M := TSQLRecordMany(Props.ManyFields[f].GetInstance(self));
  29035. if M=nil then
  29036. raise EORMException.CreateUTF8('%.Create should have created %:% for EnginePrepareMany',
  29037. [self,Props.ManyFields[f].Name,Props.ManyFields[f].ObjectClass]);
  29038. fFill.fTableMapRecordManyInstances[f] := M;
  29039. Objects[f*2+1] := M;
  29040. ObjectsClass[f*2+1] := PSQLRecordClass(M)^;
  29041. with M.RecordProps do begin
  29042. if (fRecordManySourceProp.ObjectClass<>PClass(self)^) or
  29043. (fRecordManyDestProp.ObjectClass=nil) then
  29044. raise EORMException.CreateUTF8('%.EnginePrepareMany %:% mismatch',
  29045. [self,Props.ManyFields[f].Name,Props.ManyFields[f].ObjectClass]);
  29046. ObjectsClass[f*2+2] := TSQLRecordClass(fRecordManyDestProp.ObjectClass);
  29047. D := TSQLRecordClass(fRecordManyDestProp.ObjectClass).Create;
  29048. // let TSQLRecordMany.Source and Dest point to real instances
  29049. M.fSourceID^ := PtrInt(self);
  29050. M.fDestID^ := PtrInt(D);
  29051. end;
  29052. Objects[f*2+2] := TSQLRecord(M.fDestID^);
  29053. if Props.fSQLFillPrepareMany='' then begin
  29054. aMany := AnsiChar(f*2+66); // Many=B,D,F...
  29055. if aSQLWhere<>'' then
  29056. aSQLWhere := aSQLWhere+' and ';
  29057. aSQLWhere := FormatUTF8('%%.Source=A.RowID and %.Dest=%.RowID',
  29058. [aSQLWhere,aMany,aMany,AnsiChar(f*2+67){Dest=C,E,G..}]);
  29059. end;
  29060. end;
  29061. SQLFieldsCount := 0;
  29062. aField := 'A00';
  29063. for f := 0 to high(ObjectsClass) do
  29064. with ObjectsClass[f].RecordProps do begin
  29065. PWord(@aField[2])^ := ord('I')+ord('D')shl 8;
  29066. if not AddField(nil) then
  29067. Exit; // try to add the ID field
  29068. if Props.fSQLFillPrepareMany='' then begin
  29069. if aSQLFields<>'' then
  29070. aSQLFields := aSQLFields+',';
  29071. aSQLFields := FormatUTF8('%%.RowID %',[aSQLFields,aField[1],aField]);
  29072. end;
  29073. for i := 0 to high(SimpleFields) do
  29074. with SimpleFields[i] do begin
  29075. if (f and 1=0) {self/dest} or
  29076. not(IdemPropNameU(Name,'SOURCE') or
  29077. IdemPropNameU(Name,'DEST')) {many} then begin
  29078. PWord(@aField[2])^ := TwoDigitLookupW[i];
  29079. if not AddField(SimpleFields[i]) then
  29080. Exit; // try to add this simple field
  29081. if Props.fSQLFillPrepareMany='' then
  29082. aSQLFields := FormatUTF8('%,%.% %',[aSQLFields,aField[1],Name,aField]);
  29083. end;
  29084. end;
  29085. if Props.fSQLFillPrepareMany='' then begin
  29086. if aSQLFrom<>'' then
  29087. aSQLFrom := aSQLFrom+',';
  29088. aSQLFrom := aSQLFrom+SQLTableName+' '+ToUTF8(aField[1]);
  29089. end;
  29090. inc(aField[1]);
  29091. end;
  29092. if Props.fSQLFillPrepareMany<>'' then
  29093. SQL := Props.fSQLFillPrepareMany else begin
  29094. FormatUTF8('select % from % where %',[aSQLFields,aSQLFrom,aSQLWhere],SQL);
  29095. Props.fSQLFillPrepareMany := SQL;
  29096. end;
  29097. // process aFormatSQLJoin,aParamsSQLJoin and aBoundsSQLJoin parameters
  29098. if aFormatSQLJoin<>'' then begin
  29099. aSQLWhere := '';
  29100. aSQLJoin := FormatUTF8(aFormatSQLJoin, aParamsSQLJoin);
  29101. JBeg := pointer(aSQLJoin);
  29102. repeat
  29103. J := JBeg;
  29104. while not (ord(J^) in IsIdentifier) do begin
  29105. case J^ of
  29106. '"': repeat inc(J) until J^ in [#0,'"'];
  29107. '''': repeat inc(J) until J^ in [#0,''''];
  29108. end;
  29109. if J^=#0 then break;
  29110. inc(J);
  29111. end;
  29112. if J<>JBeg then begin // append ' ',')'..
  29113. SetString(aSQLFrom,PAnsiChar(JBeg),J-JBeg);
  29114. aSQLWhere := aSQLWhere+aSQLFrom;
  29115. JBeg := J;
  29116. end;
  29117. if J^=#0 then break;
  29118. aSQLWhere := aSQLWhere+ProcessField(JBeg);
  29119. until JBeg^=#0;
  29120. SQL := SQL+' and ('+FormatUTF8(aSQLWhere,[],aBoundsSQLJoin)+')';
  29121. end;
  29122. // execute SQL statement and retrieve the matching data
  29123. result := aClient.EngineList(SQL);
  29124. if result<>'' then // prepare Fill mapping on success - see FillPrepareMany()
  29125. for i := 0 to SQLFieldsCount-1 do
  29126. with SQLFields[i] do
  29127. fFill.AddMap(Instance,Prop,i);
  29128. end;
  29129. function TSQLRecord.FillPrepareMany(aClient: TSQLRest;
  29130. const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const): boolean;
  29131. var JSON,SQL: RawUTF8;
  29132. ObjectsClass: TSQLRecordClassDynArray;
  29133. T: TSQLTable;
  29134. begin
  29135. result := false;
  29136. JSON := EnginePrepareMany(aClient,aFormatSQLJoin,aParamsSQLJoin,aBoundsSQLJoin,
  29137. ObjectsClass,SQL);
  29138. if JSON='' then
  29139. exit;
  29140. T := TSQLTableJSON.CreateFromTables(ObjectsClass,SQL,JSON);
  29141. if (T=nil) or (T.fResults=nil) then begin
  29142. T.Free;
  29143. exit;
  29144. end;
  29145. { assert(T.FieldCount=SQLFieldsCount);
  29146. for i := 0 to SQLFieldsCount-1 do
  29147. assert(IdemPropName(SQLFields[i].SQL,T.fResults[i],StrLen(T.fResults[i]))); }
  29148. fFill.fTable := T;
  29149. T.OwnerMustFree := true;
  29150. fFill.fFillCurrentRow := 1; // point to first data row (0 is field names)
  29151. result := true;
  29152. end;
  29153. function TSQLRecord.GetID: TID;
  29154. begin
  29155. {$ifdef MSWINDOWS}
  29156. if PtrUInt(self)<PtrUInt(SystemInfo.lpMinimumApplicationAddress) then
  29157. // was called from a TSQLRecord property (sftID type)
  29158. // (will return 0 if current instance is nil)
  29159. result := PtrUInt(self) else
  29160. result := fID;
  29161. // was called from a real TSQLRecord instance
  29162. {$else}
  29163. if PtrUInt(self)<$100000 then // rough estimation, but works in practice
  29164. result := PtrUInt(self) else
  29165. try
  29166. result := fID;
  29167. except
  29168. result := PtrUInt(self);
  29169. end;
  29170. {$endif}
  29171. end;
  29172. function TSQLRecord.GetIDAsPointer: pointer;
  29173. begin
  29174. {$ifdef MSWINDOWS}
  29175. if PtrUInt(self)<PtrUInt(SystemInfo.lpMinimumApplicationAddress) then
  29176. // was called from a TSQLRecord property (sftID type)
  29177. // (will return 0 if current instance is nil)
  29178. result := self else
  29179. // was called from a real TSQLRecord instance
  29180. {$ifndef CPU64}
  29181. if fID>MaxInt then
  29182. raise EORMException.CreateUTF8('%.GetIDAsPointer is storing ID=%, which '+
  29183. 'cannot be stored in a pointer/TSQLRecord 32 bit instance: use '+
  29184. 'a TID/T*ID published field for 64-bit IDs',[self,fID]) else
  29185. {$endif}
  29186. result := pointer(fID);
  29187. {$else}
  29188. if PtrUInt(self)<$100000 then // rough estimation, but works in practice
  29189. result := self else
  29190. try
  29191. result := pointer(fID);
  29192. except
  29193. result := self;
  29194. end;
  29195. {$endif}
  29196. end;
  29197. class procedure TSQLRecord.InternalRegisterCustomProperties(Props: TSQLRecordProperties);
  29198. begin // do nothing by default
  29199. end;
  29200. class procedure TSQLRecord.InternalDefineModel(Props: TSQLRecordProperties);
  29201. begin // do nothing by default
  29202. end;
  29203. function TSQLRecord.GetHasBlob: boolean;
  29204. begin
  29205. if Self=nil then
  29206. result := false else
  29207. result := RecordProps.BlobFields<>nil;
  29208. end;
  29209. function TSQLRecord.GetSimpleFieldCount: integer;
  29210. begin
  29211. if Self=nil then
  29212. result := 0 else
  29213. result := length(RecordProps.SimpleFields);
  29214. end;
  29215. function TSQLRecord.GetFillCurrentRow: integer;
  29216. begin
  29217. if (self=nil) or (fFill=nil) then
  29218. result := 0 else
  29219. result := fFill.FillCurrentRow;
  29220. end;
  29221. function TSQLRecord.GetTable: TSQLTable;
  29222. begin
  29223. if (self=nil) or (fFill=nil) then
  29224. result := nil else
  29225. result := fFill.Table;
  29226. end;
  29227. function TSQLRecord.GetFieldValue(const PropName: RawUTF8): RawUTF8;
  29228. var P: TSQLPropInfo;
  29229. begin
  29230. result := '';
  29231. if self=nil then
  29232. exit;
  29233. P := RecordProps.Fields.ByName(pointer(PropName));
  29234. if P<>nil then
  29235. P.GetValueVar(self,False,result,nil);
  29236. end;
  29237. procedure TSQLRecord.SetFieldValue(const PropName: RawUTF8; Value: PUTF8Char);
  29238. var P: TSQLPropInfo;
  29239. begin
  29240. if self=nil then
  29241. exit;
  29242. P := RecordProps.Fields.ByName(pointer(PropName));
  29243. if P<>nil then
  29244. P.SetValue(self,Value,false);
  29245. end;
  29246. {$ifndef NOVARIANTS}
  29247. function TSQLRecord.GetAsDocVariant(withID: boolean;
  29248. const withFields: TSQLFieldBits; options: PDocVariantOptions): variant;
  29249. begin
  29250. GetAsDocVariant(withID,withFields,result,options);
  29251. end;
  29252. procedure TSQLRecord.GetAsDocVariant(withID: boolean;
  29253. const withFields: TSQLFieldBits; var result: variant; options: PDocVariantOptions);
  29254. var f: integer;
  29255. Fields: TSQLPropInfoList;
  29256. doc: TDocVariantData absolute result;
  29257. begin
  29258. VarClear(result);
  29259. if self=nil then
  29260. exit;
  29261. Fields := RecordProps.Fields;
  29262. doc.InitFast(Fields.Count+1,dvObject);
  29263. if options<>nil then // force options
  29264. PDocVariantData(@result)^.Options := options^;
  29265. if withID then
  29266. doc.Values[doc.InternalAdd('RowID')] := fID;
  29267. for f := 0 to Fields.Count-1 do
  29268. if f in withFields then
  29269. Fields.List[f].GetVariant(self,doc.Values[doc.InternalAdd(Fields.List[f].Name)]);
  29270. end;
  29271. function TSQLRecord.GetSimpleFieldsAsDocVariant(withID: boolean;
  29272. options: PDocVariantOptions): variant;
  29273. begin
  29274. if self=nil then
  29275. VarClear(result) else
  29276. GetAsDocVariant(withID,RecordProps.SimpleFieldsBits[soSelect],result,options);
  29277. end;
  29278. function TSQLRecord.GetFieldVariant(const PropName: string): Variant;
  29279. var P: TSQLPropInfo;
  29280. begin
  29281. if self=nil then
  29282. P := nil else
  29283. P := RecordProps.Fields.ByRawUTF8Name({$ifdef UNICODE}StringToUTF8{$endif}(PropName));
  29284. if P=nil then
  29285. VarClear(result) else
  29286. P.GetVariant(self,result);
  29287. end;
  29288. procedure TSQLRecord.SetFieldVariant(const PropName: string;
  29289. const Source: Variant);
  29290. var P: TSQLPropInfo;
  29291. begin
  29292. if self=nil then
  29293. P := nil else
  29294. P := RecordProps.Fields.ByRawUTF8Name({$ifdef UNICODE}StringToUTF8{$endif}(PropName));
  29295. if P<>nil then
  29296. P.SetVariant(self,Source);
  29297. end;
  29298. {$endif NOVARIANTS}
  29299. function PropsCreate(aTable: TSQLRecordClass): TSQLRecordProperties;
  29300. var PVMT: pointer;
  29301. begin // private sub function makes the code faster in most case
  29302. if not aTable.InheritsFrom(TSQLRecord) then begin
  29303. result := nil; // invalid call
  29304. exit;
  29305. end;
  29306. // create the properties information from RTTI
  29307. result := TSQLRecordProperties.Create(aTable);
  29308. // store the TSQLRecordProperties instance into AutoTable unused VMT entry
  29309. PVMT := pointer(PtrInt(aTable)+vmtAutoTable);
  29310. if PPointer(PVMT)^<>nil then
  29311. raise ESynException.CreateUTF8('%.AutoTable VMT entry already set',[aTable]);
  29312. PatchCodePtrUInt(PVMT,PtrUInt(result),true); // LeaveUnprotected=true
  29313. // register to the internal garbage collection (avoid memory leak)
  29314. GarbageCollectorFreeAndNil(PVMT^,result); // set to nil at finalization
  29315. // overriden method may use RecordProps -> do it after the VMT is set
  29316. aTable.InternalDefineModel(result);
  29317. end;
  29318. // since "var class" are not available in Delphi 6-7, and is inherited by
  29319. // the children classes under latest Delphi versions (i.e. the "var class" is
  29320. // shared by all inherited classes, whereas we want one var per class), we reused
  29321. // one of the unused magic VMT slots (i.e. the one for automated methods,
  29322. // AutoTable, a relic from Delphi 2 that is generally not used anymore) - see
  29323. // http://hallvards.blogspot.com/2007/05/hack17-virtual-class-variables-part-ii.html
  29324. {$ifdef FPC_OR_PUREPASCAL}
  29325. class function TSQLRecord.RecordProps: TSQLRecordProperties;
  29326. begin
  29327. if Self<>nil then begin
  29328. result := PPointer(PtrInt(Self)+vmtAutoTable)^;
  29329. if result=nil then
  29330. result := PropsCreate(self);
  29331. end else
  29332. result := nil;
  29333. end;
  29334. {$else}
  29335. class function TSQLRecord.RecordProps: TSQLRecordProperties;
  29336. asm
  29337. test eax,eax
  29338. jz @null
  29339. mov edx,[eax+vmtAutoTable]
  29340. test edx,edx
  29341. jz PropsCreate
  29342. mov eax,edx
  29343. @null:
  29344. end;
  29345. {$endif}
  29346. function TSQLRecord.Filter(const aFields: TSQLFieldBits): boolean;
  29347. var f, i: integer;
  29348. Value, Old: RawUTF8;
  29349. begin
  29350. result := IsZero(aFields);
  29351. if (self=nil) or result then
  29352. // avoid GPF and handle case if no field was selected
  29353. exit;
  29354. with RecordProps do
  29355. if Filters=nil then
  29356. // no filter set yet -> process OK
  29357. result := true else begin
  29358. for f := 0 to Fields.Count-1 do
  29359. if (Fields.List[f].SQLFieldType in COPIABLE_FIELDS) then
  29360. for i := 0 to length(Filters[f])-1 do
  29361. if Filters[f,i].InheritsFrom(TSynFilter) then begin
  29362. Fields.List[f].GetValueVar(self,false,Value,nil);
  29363. Old := Value;
  29364. TSynFilter(Filters[f,i]).Process(f,Value);
  29365. if Old<>Value then
  29366. // value was changed -> store modified
  29367. Fields.List[f].SetValueVar(self,Value,false);
  29368. end;
  29369. end;
  29370. end;
  29371. function TSQLRecord.Filter(const aFields: array of RawUTF8): boolean;
  29372. var F: TSQLFieldBits;
  29373. begin
  29374. if RecordProps.FieldBitsFromRawUTF8(aFields,F) then
  29375. // must always call the virtual Filter() method
  29376. result := Filter(F) else
  29377. result := false;
  29378. end;
  29379. class function TSQLRecord.SQLTableName: RawUTF8;
  29380. begin
  29381. if self=nil then
  29382. result := '' else
  29383. result := RecordProps.SQLTableName;
  29384. end;
  29385. class function TSQLRecord.AutoFree(varClassPairs: array of pointer): IAutoFree;
  29386. var n,i: integer;
  29387. begin
  29388. n := length(varClassPairs);
  29389. if (n=0) or (n and 1=1) then
  29390. exit;
  29391. n := n shr 1;
  29392. if n=0 then
  29393. exit;
  29394. for i := 0 to n-1 do // convert TSQLRecordClass into TSQLRecord instances
  29395. varClassPairs[i*2+1] := TSQLRecordClass(varClassPairs[i*2+1]).Create;
  29396. result := TAutoFree.Create(varClassPairs);
  29397. end;
  29398. class function TSQLRecord.AutoFree(var localVariable): IAutoFree;
  29399. begin
  29400. result := TAutoFree.Create(localVariable,Create);
  29401. end;
  29402. class function TSQLRecord.AutoFree(var localVariable; Rest: TSQLRest; ID: TID): IAutoFree;
  29403. begin
  29404. result := TAutoFree.Create(localVariable,Create(Rest,ID));
  29405. end;
  29406. class procedure TSQLRecord.AddFilterOrValidate(const aFieldName: RawUTF8;
  29407. aFilter: TSynFilterOrValidate);
  29408. begin
  29409. RecordProps.AddFilterOrValidate(aFieldName,aFilter);
  29410. end;
  29411. class procedure TSQLRecord.AddFilterNotVoidText(const aFieldNames: array of RawUTF8);
  29412. var i,f: Integer;
  29413. begin
  29414. with RecordProps do
  29415. for i := 0 to high(aFieldNames) do begin
  29416. f := Fields.IndexByNameOrExcept(aFieldNames[i]);
  29417. AddFilterOrValidate(f,TSynFilterTrim.Create);
  29418. AddFilterOrValidate(f,TSynValidateNonVoidText.Create);
  29419. end;
  29420. end;
  29421. class procedure TSQLRecord.AddFilterNotVoidAllTextFields;
  29422. var f: Integer;
  29423. begin
  29424. with RecordProps,Fields do
  29425. for f := 0 to Count-1 do
  29426. if List[f].SQLFieldType in RAWTEXT_FIELDS then begin
  29427. AddFilterOrValidate(f,TSynFilterTrim.Create);
  29428. AddFilterOrValidate(f,TSynValidateNonVoidText.Create);
  29429. end;
  29430. end;
  29431. function TSQLRecord.Validate(aRest: TSQLRest; const aFields: TSQLFieldBits;
  29432. aInvalidFieldIndex: PInteger; aValidator: PSynValidate): string;
  29433. var f, i: integer;
  29434. Value: RawUTF8;
  29435. Validate: TSynValidate;
  29436. ValidateRest: TSynValidateRest absolute Validate;
  29437. wasTSynValidateRest: boolean;
  29438. begin
  29439. result := '';
  29440. if (self=nil) or IsZero(aFields) then
  29441. // avoid GPF and handle case if no field was selected
  29442. exit;
  29443. Value := ''; // alf: to circumvent FPC issues
  29444. with RecordProps do
  29445. if Filters<>nil then
  29446. for f := 0 to Fields.Count-1 do
  29447. if Fields.List[f].SQLFieldType in COPIABLE_FIELDS then begin
  29448. for i := 0 to length(Filters[f])-1 do begin
  29449. Validate := TSynValidate(Filters[f,i]);
  29450. if Validate.InheritsFrom(TSynValidate) then begin
  29451. if Value='' then
  29452. Fields.List[f].GetValueVar(self,false,Value,nil);
  29453. wasTSynValidateRest := Validate.InheritsFrom(TSynValidateRest);
  29454. if wasTSynValidateRest then begin // set additional parameters
  29455. ValidateRest.fProcessRec := self;
  29456. ValidateRest.fProcessRest := aRest;
  29457. end;
  29458. try
  29459. if not Validate.Process(f,Value,result) then begin
  29460. // TSynValidate process failed -> notify caller
  29461. if aInvalidFieldIndex<>nil then
  29462. aInvalidFieldIndex^ := f;
  29463. if aValidator<>nil then
  29464. aValidator^ := Validate;
  29465. if result='' then
  29466. // no custom message -> show a default message
  29467. result := format(sValidationFailed,[
  29468. GetCaptionFromClass(Validate.ClassType)]);
  29469. exit;
  29470. end;
  29471. finally
  29472. if wasTSynValidateRest then begin // reset additional parameters
  29473. ValidateRest.fProcessRec := nil;
  29474. ValidateRest.fProcessRest := nil;
  29475. end;
  29476. end;
  29477. end;
  29478. end;
  29479. Value := '';
  29480. end;
  29481. end;
  29482. function TSQLRecord.Validate(aRest: TSQLRest; const aFields: array of RawUTF8;
  29483. aInvalidFieldIndex: PInteger; aValidator: PSynValidate): string;
  29484. var F: TSQLFieldBits;
  29485. begin
  29486. if RecordProps.FieldBitsFromRawUTF8(aFields,F) then
  29487. // must always call the virtual Validate() method
  29488. result := Validate(aRest,F,aInvalidFieldIndex,aValidator) else
  29489. result := '';
  29490. end;
  29491. function TSQLRecord.FilterAndValidate(aRest: TSQLRest; out aErrorMessage: string;
  29492. const aFields: TSQLFieldBits; aValidator: PSynValidate): boolean;
  29493. var invalidField: Integer;
  29494. begin
  29495. Filter(aFields);
  29496. aErrorMessage := Validate(aRest,aFields,@invalidField,aValidator);
  29497. if aErrorMessage='' then
  29498. result := true else begin
  29499. if invalidField>=0 then
  29500. aErrorMessage := Format('"%s": %s',
  29501. [RecordProps.Fields.List[invalidField].GetNameDisplay,aErrorMessage]);
  29502. result := false;
  29503. end;
  29504. end;
  29505. function TSQLRecord.FilterAndValidate(aRest: TSQLRest;
  29506. const aFields: TSQLFieldBits; aValidator: PSynValidate): RawUTF8;
  29507. var msg: string;
  29508. begin
  29509. if FilterAndValidate(aRest,msg,aFields,aValidator) then
  29510. result := '' else
  29511. StringToUTF8(msg,result);
  29512. end;
  29513. function TSQLRecord.DynArray(const DynArrayFieldName: RawUTF8): TDynArray;
  29514. var F: integer;
  29515. begin
  29516. with RecordProps do
  29517. for F := 0 to high(DynArrayFields) do
  29518. with DynArrayFields[F] do
  29519. if IdemPropNameU(Name,DynArrayFieldName) then begin
  29520. result := GetDynArray(self);
  29521. exit;
  29522. end;
  29523. result.Void;
  29524. end;
  29525. function TSQLRecord.DynArray(DynArrayFieldIndex: integer): TDynArray;
  29526. var F: integer;
  29527. begin
  29528. if DynArrayFieldIndex>0 then
  29529. with RecordProps do
  29530. for F := 0 to high(DynArrayFields) do
  29531. with DynArrayFields[F] do
  29532. if DynArrayIndex=DynArrayFieldIndex then begin
  29533. result := GetDynArray(self);
  29534. exit;
  29535. end;
  29536. result.Void;
  29537. end;
  29538. procedure TSQLRecord.ComputeFieldsBeforeWrite(aRest: TSQLRest; aOccasion: TSQLEvent);
  29539. var F: integer;
  29540. types: TSQLFieldTypes;
  29541. i64: Int64;
  29542. begin
  29543. if (self<>nil) and (aRest<>nil) then
  29544. with RecordProps do begin
  29545. integer(types) := 0;
  29546. if sftModTime in HasTypeFields then
  29547. include(types,sftModTime);
  29548. if (sftCreateTime in HasTypeFields) and (aOccasion=seAdd) then
  29549. include(types,sftCreateTime);
  29550. if integer(types)<>0 then begin
  29551. i64 := aRest.ServerTimeStamp;
  29552. for F := 0 to Fields.Count-1 do
  29553. with TSQLPropInfoRTTIInt64(Fields.List[f]) do
  29554. if SQLFieldType in types then
  29555. fPropInfo.SetInt64Prop(Self,i64);
  29556. end;
  29557. if sftSessionUserID in HasTypeFields then begin
  29558. i64 := aRest.GetCurrentSessionUserID;
  29559. if i64<>0 then
  29560. for F := 0 to Fields.Count-1 do
  29561. with TSQLPropInfoRTTIInt64(Fields.List[f]) do
  29562. if SQLFieldType=sftSessionUserID then
  29563. fPropInfo.SetInt64Prop(Self,i64);
  29564. end;
  29565. end;
  29566. end;
  29567. { TSQLRecordPropertiesMapping }
  29568. procedure TSQLRecordPropertiesMapping.Init(Table: TSQLRecordClass;
  29569. const MappedTableName: RawUTF8; MappedConnection: TObject;
  29570. AutoComputeSQL: boolean);
  29571. begin
  29572. fProps := Table.RecordProps;
  29573. if MappedTableName='' then
  29574. fTableName := fProps.SQLTableName else
  29575. fTableName := MappedTableName;
  29576. fConnectionProperties := MappedConnection;
  29577. fRowIDFieldName := 'ID';
  29578. fProps.Fields.NamesToRawUTF8DynArray(fFieldNames);
  29579. FillcharFast(fFieldNamesMatchInternal,sizeof(fFieldNamesMatchInternal),255);
  29580. fAutoComputeSQL := AutoComputeSQL;
  29581. fMappingVersion := 1;
  29582. if fAutoComputeSQL then
  29583. ComputeSQL;
  29584. end;
  29585. function TSQLRecordPropertiesMapping.MapField(
  29586. const InternalName, ExternalName: RawUTF8): PSQLRecordPropertiesMapping;
  29587. begin
  29588. MapFields([InternalName,ExternalName]);
  29589. result := @self;
  29590. end;
  29591. function TSQLRecordPropertiesMapping.MapFields(
  29592. const InternalExternalPairs: array of RawUTF8): PSQLRecordPropertiesMapping;
  29593. var i,int: Integer;
  29594. begin
  29595. for i := 0 to (length(InternalExternalPairs) shr 1)-1 do begin
  29596. int := fProps.Fields.IndexByNameOrExcept(InternalExternalPairs[i*2]);
  29597. if int<0 then begin
  29598. fRowIDFieldName := InternalExternalPairs[i*2+1];
  29599. if IdemPropNameU(fRowIDFieldName,'ID') then
  29600. include(fFieldNamesMatchInternal,0) else // [0]=ID
  29601. exclude(fFieldNamesMatchInternal,0);
  29602. end else begin
  29603. fFieldNames[int] := InternalExternalPairs[i*2+1];
  29604. if IdemPropNameU(fFieldNames[int],fProps.Fields.List[int].Name) then
  29605. include(fFieldNamesMatchInternal,int+1) else // [0]=ID [1..n]=fields[i-1]
  29606. exclude(fFieldNamesMatchInternal,int+1);
  29607. end;
  29608. end;
  29609. inc(fMappingVersion);
  29610. if fAutoComputeSQL then
  29611. ComputeSQL;
  29612. result := @self;
  29613. end;
  29614. function TSQLRecordPropertiesMapping.MapAutoKeywordFields:
  29615. PSQLRecordPropertiesMapping;
  29616. begin
  29617. if @self<>nil then
  29618. include(fOptions,rpmAutoMapKeywordFields);
  29619. result := @self;
  29620. end;
  29621. function TSQLRecordPropertiesMapping.SetOptions(
  29622. aOptions: TSQLRecordPropertiesMappingOptions): PSQLRecordPropertiesMapping;
  29623. begin
  29624. if @self<>nil then
  29625. fOptions := aOptions;
  29626. result := @self;
  29627. end;
  29628. procedure TSQLRecordPropertiesMapping.ComputeSQL;
  29629. type // similar to TSQLModelRecordProperties.Create()/SetKind()
  29630. TContent = (TableSimpleFields, UpdateSimple, UpdateSetAll, InsertAll);
  29631. procedure SetSQL(W: TTextWriter;
  29632. withID, withTableName: boolean; var result: RawUTF8;
  29633. content: TContent=TableSimpleFields);
  29634. var f: integer;
  29635. begin
  29636. W.CancelAll;
  29637. if withID and (content=TableSimpleFields) then begin
  29638. if withTableName then
  29639. W.AddStrings([TableName,'.']);
  29640. W.AddString(RowIDFieldName);
  29641. if 0 in FieldNamesMatchInternal then
  29642. W.Add(',') else
  29643. W.AddShort(' as ID,');
  29644. end;
  29645. with fProps do
  29646. for f := 0 to Fields.Count-1 do
  29647. with Fields.List[f] do
  29648. if SQLFieldType in COPIABLE_FIELDS then // sftMany fields do not exist
  29649. case content of
  29650. TableSimpleFields:
  29651. if f in SimpleFieldsBits[soSelect] then begin
  29652. if withTableName then
  29653. W.AddStrings([TableName,'.']);
  29654. W.AddString(FieldNames[f]);
  29655. if not(f+1 in FieldNamesMatchInternal) then
  29656. W.AddStrings([' as ',Name]); // to get expected JSON column name
  29657. W.Add(',');
  29658. end;
  29659. UpdateSimple:
  29660. if f in SimpleFieldsBits[soSelect] then
  29661. W.AddStrings([FieldNames[f],'=?,']);
  29662. UpdateSetAll:
  29663. W.AddStrings([FieldNames[f],'=?,']);
  29664. InsertAll:
  29665. W.AddStrings([FieldNames[f],',']);
  29666. end;
  29667. W.CancelLastComma;
  29668. W.SetText(result);
  29669. end;
  29670. var W: TTextWriter;
  29671. begin
  29672. W := TTextWriter.CreateOwnedStream(1024);
  29673. try // SQL.TableSimpleFields[withID: boolean; withTableName: boolean]
  29674. SetSQL(W,false,false,fSQL.TableSimpleFields[false,false]);
  29675. SetSQL(W,false,true,fSQL.TableSimpleFields[false,true]);
  29676. SetSQL(W,true,false,fSQL.TableSimpleFields[true,false]);
  29677. SetSQL(W,true,true,fSQL.TableSimpleFields[true,true]);
  29678. // SQL.SelectAll: array[withRowID: boolean]
  29679. fSQL.SelectAllWithRowID := SQLFromSelect(TableName,'*','',
  29680. fSQL.TableSimpleFields[true,false]);
  29681. fSQL.SelectAllWithID := fSQL.SelectAllWithRowID;
  29682. SetSQL(W,false,false,fSQL.UpdateSetSimple,UpdateSimple);
  29683. SetSQL(W,false,false,fSQL.UpdateSetAll,UpdateSetAll);
  29684. SetSQL(W,false,false,fSQL.InsertSet,InsertAll);
  29685. finally
  29686. W.Free;
  29687. end;
  29688. end;
  29689. function TSQLRecordPropertiesMapping.InternalToExternal(const FieldName: RawUTF8): RawUTF8;
  29690. var int: integer;
  29691. begin
  29692. int := fProps.Fields.IndexByNameOrExcept(FieldName);
  29693. if int<0 then
  29694. result := RowIDFieldName else
  29695. result := fFieldNames[int];
  29696. end;
  29697. function TSQLRecordPropertiesMapping.InternalCSVToExternalCSV(
  29698. const CSVFieldNames, Sep, SepEnd: RawUTF8): RawUTF8;
  29699. var IntFields,ExtFields: TRawUTF8DynArray;
  29700. begin
  29701. CSVToRawUTF8DynArray(CSVFieldNames,Sep,SepEnd,IntFields);
  29702. InternalToExternalDynArray(IntFields,ExtFields);
  29703. result := RawUTF8ArrayToCSV(ExtFields,Sep)+SepEnd;
  29704. end;
  29705. procedure TSQLRecordPropertiesMapping.InternalToExternalDynArray(
  29706. const IntFieldNames: array of RawUTF8; out result: TRawUTF8DynArray;
  29707. IntFieldIndex: PIntegerDynArray);
  29708. var i,n,ndx: integer;
  29709. begin
  29710. n := length(IntFieldNames);
  29711. SetLength(result,n);
  29712. if IntFieldIndex<>nil then
  29713. SetLength(IntFieldIndex^,n);
  29714. for i := 0 to n-1 do begin
  29715. ndx := fProps.Fields.IndexByNameOrExcept(IntFieldNames[i]);
  29716. if IntFieldIndex<>nil then
  29717. IntFieldIndex^[i] := ndx;
  29718. if ndx<0 then
  29719. result[i] := RowIDFieldName else
  29720. result[i] := fFieldNames[ndx];
  29721. end;
  29722. end;
  29723. function TSQLRecordPropertiesMapping.ExternalToInternalIndex(
  29724. const ExtFieldName: RawUTF8): integer;
  29725. begin
  29726. if IdemPropNameU(ExtFieldName,RowIDFieldName) then
  29727. result := -1 else begin
  29728. // search for customized field mapping
  29729. for result := 0 to high(fFieldNames) do
  29730. if IdemPropNameU(ExtFieldName,UnQuotedSQLSymbolName(fFieldNames[result])) then
  29731. exit;
  29732. result := -2; // indicates not found
  29733. end;
  29734. end;
  29735. function TSQLRecordPropertiesMapping.ExternalToInternalOrNull(
  29736. const ExtFieldName: RawUTF8): RawUTF8;
  29737. var i: integer;
  29738. begin
  29739. i := ExternalToInternalIndex(ExtFieldName);
  29740. if i=-1 then
  29741. result := 'ID' else
  29742. if i>=0 then
  29743. result := fProps.Fields.List[i].Name else
  29744. result := ''; // indicates not found
  29745. end;
  29746. function TSQLRecordPropertiesMapping.AppendFieldName(
  29747. FieldIndex: Integer; var Text: RawUTF8): boolean;
  29748. begin
  29749. result := false; // success
  29750. if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN then
  29751. Text := Text+RowIDFieldName else
  29752. if cardinal(FieldIndex)>=cardinal(Length(FieldNames)) then
  29753. result := true else // FieldIndex out of range
  29754. Text := Text+FieldNames[FieldIndex];
  29755. end;
  29756. function TSQLRecordPropertiesMapping.FieldNameByIndex(FieldIndex: Integer): RawUTF8;
  29757. begin
  29758. if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN then
  29759. result := RowIDFieldName else
  29760. if cardinal(FieldIndex)>=cardinal(Length(FieldNames)) then
  29761. result := '' else // FieldIndex out of range
  29762. result := FieldNames[FieldIndex];
  29763. end;
  29764. { TSQLModelRecordProperties }
  29765. constructor TSQLModelRecordProperties.Create(aModel: TSQLModel;
  29766. aTable: TSQLRecordClass; aKind: TSQLRecordVirtualKind);
  29767. var f: integer;
  29768. begin // similar to TSQLRecordPropertiesMapping.ComputeSQL
  29769. fModel := aModel;
  29770. fTableIndex := fModel.GetTableIndexExisting(aTable);
  29771. fProps := aTable.RecordProps;
  29772. SetKind(aKind);
  29773. with Props do
  29774. for f := 0 to Fields.Count-1 do
  29775. with Fields.List[f] do
  29776. if SQLFieldType in COPIABLE_FIELDS then begin // sftMany fields do not exist
  29777. // pre-computation of SQL statements
  29778. SQL.UpdateSetAll := SQL.UpdateSetAll+Name+'=?,';
  29779. SQL.InsertSet := SQL.InsertSet+Name+',';
  29780. if f in SimpleFieldsBits[soUpdate] then
  29781. SQL.UpdateSetSimple := SQL.UpdateSetSimple+Name+'=?,';
  29782. // filter + validation of unique fields, i.e. if marked as "stored false"
  29783. if f in IsUniqueFieldsBits then begin
  29784. // must trim() text value before storage, and validate for unicity
  29785. if SQLFieldType in [sftUTF8Text,sftAnsiText] then
  29786. AddFilterOrValidate(f,TSynFilterTrim.Create);
  29787. // register unique field pre-validation
  29788. AddFilterOrValidate(f,TSynValidateUniqueField.Create);
  29789. end;
  29790. end;
  29791. SetLength(SQL.InsertSet,length(SQL.InsertSet)-1);
  29792. SetLength(SQL.UpdateSetAll,length(SQL.UpdateSetAll)-1); // 'COL1=?,COL2=?'
  29793. if SQL.UpdateSetSimple<>'' then
  29794. SetLength(SQL.UpdateSetSimple,length(SQL.UpdateSetSimple)-1); // 'COL1=?,COL2=?'
  29795. Props.InternalRegisterModel(aModel,aModel.GetTableIndexExisting(aTable),self);
  29796. end;
  29797. constructor TSQLModelRecordProperties.CreateFrom(aModel: TSQLModel;
  29798. aSource: TSQLModelRecordProperties);
  29799. begin
  29800. inherited Create;
  29801. fModel := aModel;
  29802. fTableIndex := aSource.fTableIndex;
  29803. fFTSWithoutContentTableIndex := aSource.fFTSWithoutContentTableIndex;
  29804. fFTSWithoutContentFields := aSource.fFTSWithoutContentFields;
  29805. fProps := aSource.fProps;
  29806. fKind := aSource.Kind;
  29807. SQL := aSource.SQL;
  29808. ExternalDB := aSource.ExternalDB;
  29809. Props.InternalRegisterModel(fModel,fModel.GetTableIndexExisting(fProps.Table),self);
  29810. end;
  29811. procedure TSQLModelRecordProperties.SetKind(Value: TSQLRecordVirtualKind);
  29812. function IntSQLTableSimpleFields(withID, withTableName: boolean): RawUTF8;
  29813. const IDComma: array[TSQLRecordVirtualKind] of rawUTF8 =
  29814. ('ID,','RowID,','RowID,','RowID,','RowID,','RowID,');
  29815. // rSQLite3, rFTS3, rFTS4, rRTree, rCustomForcedID, rCustomAutoID
  29816. var TableName: RawUTF8;
  29817. i: integer;
  29818. begin
  29819. if withTableName then
  29820. TableName := Props.SQLTableName+'.'; // calc TableName once
  29821. if withID then
  29822. if withTableName then
  29823. result := TableName+IDComma[Kind] else
  29824. result := IDComma[Kind] else
  29825. result := '';
  29826. for i := 0 to high(Props.SimpleFields) do begin
  29827. if withTableName then
  29828. result := result+TableName;
  29829. result := result+Props.SimpleFields[i].Name+','; // valid simple fields
  29830. end;
  29831. if result<>'' then
  29832. SetLength(result,length(result)-1); // trim last ','
  29833. end;
  29834. begin
  29835. fKind := Value;
  29836. // SQL.TableSimpleFields[withID: boolean; withTableName: boolean]
  29837. SQL.TableSimpleFields[false,false] := IntSQLTableSimpleFields(false,false);
  29838. SQL.TableSimpleFields[false,true] := IntSQLTableSimpleFields(false,true);
  29839. SQL.TableSimpleFields[true,false] := IntSQLTableSimpleFields(true,false);
  29840. SQL.TableSimpleFields[true,true] := IntSQLTableSimpleFields(true,true);
  29841. if Props.SQLTableSimpleFieldsNoRowID<>SQL.TableSimpleFields[false,false] then
  29842. raise EModelException.CreateUTF8('SetKind(%)',[Props.Table]);
  29843. SQL.SelectAllWithRowID := SQLFromSelectWhere('*','');
  29844. SQL.SelectAllWithID := SQL.SelectAllWithRowID;
  29845. if IdemPChar(PUTF8Char(pointer(SQL.SelectAllWithID))+7,'ROWID') then
  29846. delete(SQL.SelectAllWithID,8,3); // 'SELECT RowID,..' -> 'SELECT ID,'
  29847. end;
  29848. function TSQLModelRecordProperties.SQLFromSelectWhere(
  29849. const SelectFields, Where: RawUTF8): RawUTF8;
  29850. begin
  29851. result := SQLFromSelect(Props.SQLTableName,SelectFields,Where,
  29852. SQL.TableSimpleFields[true,false]);
  29853. end;
  29854. procedure TSQLModelRecordProperties.FTS4WithoutContent(ContentTable: TSQLRecordClass);
  29855. var i: integer;
  29856. field: RawUTF8;
  29857. begin
  29858. if Kind<>rFTS4 then
  29859. raise EModelException.CreateUTF8('FTS4WithoutContent: % is not a FTS4 table',[Props.Table]);
  29860. fFTSWithoutContentTableIndex := fModel.GetTableIndexExisting(ContentTable);
  29861. for i := 0 to Props.Fields.Count-1 do begin
  29862. field := Props.Fields.List[i].Name;
  29863. if ContentTable.RecordProps.Fields.IndexByName(field)<0 then
  29864. raise EModelException.CreateUTF8('FTS4WithoutContent: %.% is not a % field',
  29865. [Props.Table,field,ContentTable]);
  29866. fFTSWithoutContentFields := fFTSWithoutContentFields+',new.'+field;
  29867. end;
  29868. if fFTSWithoutContentFields='' then
  29869. raise EModelException.CreateUTF8('FTS4WithoutContent: % has no field',[Props.Table]);
  29870. end;
  29871. function TSQLModelRecordProperties.GetProp(const PropName: RawUTF8): TSQLPropInfo;
  29872. begin
  29873. if self<>nil then
  29874. result := Props.Fields.ByName(pointer(PropName)) else
  29875. result := nil;
  29876. end;
  29877. { TSQLModel }
  29878. function TSQLModel.GetTableIndexSafe(aTable: TSQLRecordClass;
  29879. RaiseExceptionIfNotExisting: boolean): integer;
  29880. begin
  29881. for result := 0 to fTablesMax do // manual search: GetTableIndex() may fail
  29882. if fTables[result]=aTable then
  29883. exit;
  29884. if RaiseExceptionIfNotExisting then
  29885. raise EModelException.CreateUTF8('% must include %',[self,aTable]);
  29886. result := -1;
  29887. end;
  29888. procedure TSQLModel.SetTableProps(aIndex: integer);
  29889. var j,f: integer;
  29890. t: TSQLFieldType;
  29891. Kind: TSQLRecordVirtualKind;
  29892. Table: TSQLRecordClass;
  29893. aTableName,aFieldName: RawUTF8;
  29894. Props: TSQLModelRecordProperties;
  29895. W: TTextWriter;
  29896. procedure RegisterTableForRecordReference(aFieldType: TSQLPropInfo;
  29897. aFieldTable: TClass);
  29898. var R: integer;
  29899. begin
  29900. if (aFieldTable=nil) or not aFieldTable.InheritsFrom(TSQLRecord) then
  29901. exit; // no associated table to track deletion
  29902. R := length(fRecordReferences);
  29903. SetLength(fRecordReferences,R+1);
  29904. with fRecordReferences[R] do begin
  29905. TableIndex := aIndex;
  29906. FieldType := aFieldType;
  29907. FieldTable := pointer(aFieldTable);
  29908. FieldTableIndex := GetTableIndexSafe(FieldTable,false);
  29909. if FieldTableIndex<0 then
  29910. FieldTableIndex := -2; // allow lazy table index identification
  29911. if aFieldType.InheritsFrom(TSQLPropInfoRTTIRecordReference) then
  29912. CascadeDelete := TSQLPropInfoRTTIRecordReference(aFieldType).CascadeDelete;
  29913. end;
  29914. end;
  29915. begin
  29916. if (cardinal(aIndex)>cardinal(fTablesMax)) or (fTableProps[aIndex]<>nil) then
  29917. raise EModelException.Create('TSQLModel.SetTableProps');
  29918. Table := fTables[aIndex];
  29919. if Table.InheritsFrom(TSQLRecordFTS4) then
  29920. Kind := rFTS4 else
  29921. if Table.InheritsFrom(TSQLRecordFTS3) then
  29922. Kind := rFTS3 else
  29923. if Table.InheritsFrom(TSQLRecordVirtualTableForcedID) then
  29924. Kind := rCustomForcedID else
  29925. if Table.InheritsFrom(TSQLRecordRTree) then
  29926. Kind := rRTree else
  29927. if Table.InheritsFrom(TSQLRecordVirtual) then
  29928. Kind := rCustomAutoID else
  29929. Kind := rSQLite3;
  29930. Props := TSQLModelRecordProperties.Create(self,Table,Kind);
  29931. Props.Props.InternalRegisterModel(Self,aIndex,Props);
  29932. for t := low(t) to high(t) do
  29933. if fCustomCollationForAll[t]<>'' then
  29934. Props.Props.SetCustomCollationForAll(t,fCustomCollationForAll[t]);
  29935. fTableProps[aIndex] := Props;
  29936. aTableName := Props.Props.SQLTableName;
  29937. fSortedTablesName[aIndex] := aTableName;
  29938. fSortedTablesNameIndex[aIndex] := aIndex;
  29939. with Props.Props.Fields do
  29940. for f := 0 to Count-1 do
  29941. case List[f].SQLFieldType of
  29942. sftRecord:
  29943. RegisterTableForRecordReference(List[f],Table); // Table not used
  29944. sftID:
  29945. RegisterTableForRecordReference(
  29946. List[f],(List[f] as TSQLPropInfoRTTIInstance).ObjectClass);
  29947. sftTID:
  29948. RegisterTableForRecordReference(
  29949. List[f],(List[f] as TSQLPropInfoRTTITID).RecordClass);
  29950. sftMany:
  29951. GetTableIndexSafe(pointer((List[f] as TSQLPropInfoRTTIMany).ObjectClass),true);
  29952. end;
  29953. if Props.Props.JoinedFieldsTable<>nil then begin
  29954. W := TTextWriter.CreateOwnedStream;
  29955. try
  29956. W.AddShort('SELECT ');
  29957. // JoinedFieldsTable[0] is the class itself
  29958. with Props.Props do begin
  29959. W.Add('%.RowID as `%.RowID`,',[SQLTableName,SQLTableName]);
  29960. for f := 0 to High(SimpleFields) do
  29961. if SimpleFields[f].SQLFieldType<>sftID then
  29962. W.Add('%.% as `%.%`,',[SQLTableName,SimpleFields[f].Name,
  29963. SQLTableName,SimpleFields[f].Name]);
  29964. end;
  29965. // add JoinedFieldsTable[1..] fields
  29966. for j := 1 to high(Props.Props.JoinedFieldsTable) do begin
  29967. aFieldName := Props.Props.JoinedFields[j-1].Name;
  29968. W.Add('%.RowID as `%.RowID`,',[aFieldName,aFieldName]);
  29969. with Props.Props.JoinedFieldsTable[j].RecordProps do
  29970. for f := 0 to High(SimpleFields) do
  29971. if SimpleFields[f].SQLFieldType<>sftID then
  29972. W.Add('%.% as `%.%`,',[aFieldName,SimpleFields[f].Name,
  29973. aFieldName,SimpleFields[f].Name]);
  29974. end;
  29975. W.CancelLastComma;
  29976. // add LEFT JOIN clause
  29977. W.AddStrings([' FROM ',aTableName]);
  29978. for j := 1 to high(Props.Props.JoinedFieldsTable) do begin
  29979. aFieldName := Props.Props.JoinedFields[j-1].Name;
  29980. with Props.Props.JoinedFieldsTable[j].RecordProps do
  29981. W.Add(' LEFT JOIN % AS % ON %.%=%.RowID',[
  29982. SQLTableName,aFieldName,aTableName,aFieldName,aFieldName]);
  29983. end;
  29984. W.SetText(Props.SQL.SelectAllJoined);
  29985. finally
  29986. W.Free;
  29987. end;
  29988. end;
  29989. end;
  29990. function TSQLModel.GetTableProps(aClass: TSQLRecordClass): TSQLModelRecordProperties;
  29991. begin
  29992. result := fTableProps[GetTableIndexExisting(aClass)];
  29993. end;
  29994. function TSQLModel.AddTable(aTable: TSQLRecordClass;
  29995. aTableIndexCreated: PInteger=nil): boolean;
  29996. var n: integer;
  29997. begin
  29998. // first register for JSONToObject() and for TSQLPropInfoRTTITID.Create()
  29999. TJSONSerializer.RegisterClassForJSON(aTable);
  30000. // insert only once
  30001. if GetTableIndex(aTable)>=0 then begin
  30002. result := false;
  30003. exit;
  30004. end;
  30005. // add to the model list
  30006. inc(fTablesMax);
  30007. n := fTablesMax+1;
  30008. SetLength(fTables,n);
  30009. SetLength(fSortedTablesName,n);
  30010. SetLength(fSortedTablesNameIndex,n);
  30011. SetLength(fTableProps,n);
  30012. fTables[fTablesMax] := aTable;
  30013. SetTableProps(fTablesMax);
  30014. QuickSortRawUTF8(fSortedTablesName,fTablesMax+1,@fSortedTablesNameIndex,@StrIComp);
  30015. if aTableIndexCreated<>nil then
  30016. aTableIndexCreated^ := fTablesMax;
  30017. result := true;
  30018. end;
  30019. function TSQLModel.AddTableInherited(aTable: TSQLRecordClass): pointer;
  30020. var ndx: integer;
  30021. begin
  30022. ndx := GetTableIndexInheritsFrom(aTable);
  30023. if ndx<0 then
  30024. if not AddTable(aTable,@ndx) then
  30025. raise EORMException.CreateUTF8('%.AddTableInherited(%)',[self,aTable]);
  30026. result := Tables[ndx];
  30027. end;
  30028. constructor TSQLModel.Create(CloneFrom: TSQLModel);
  30029. var i: integer;
  30030. begin
  30031. if CloneFrom=nil then
  30032. raise EModelException.CreateUTF8('%.Create(CloneFrom=nil)',[self]);
  30033. fTables := CloneFrom.fTables;
  30034. fTablesMax := CloneFrom.fTablesMax;
  30035. if fTablesMax<>High(fTables) then
  30036. raise EModelException.CreateUTF8('%.Create: incorrect CloneFrom.TableMax',[self]);
  30037. fRoot := CloneFrom.fRoot;
  30038. fActions := CloneFrom.fActions;
  30039. fEvents := CloneFrom.fEvents;
  30040. fRestOwner := CloneFrom.fRestOwner;
  30041. fSortedTablesName := CloneFrom.fSortedTablesName;
  30042. fSortedTablesNameIndex := CloneFrom.fSortedTablesNameIndex;
  30043. fRecordReferences := CloneFrom.fRecordReferences;
  30044. fVirtualTableModule := CloneFrom.fVirtualTableModule;
  30045. fCustomCollationForAll := CloneFrom.fCustomCollationForAll;
  30046. SetLength(fTableProps,fTablesMax+1);
  30047. for i := 0 to fTablesMax do
  30048. fTableProps[i] := TSQLModelRecordProperties.CreateFrom(
  30049. self,CloneFrom.fTableProps[i]);
  30050. end;
  30051. constructor TSQLModel.Create(Owner: TSQLRest; TabParameters: PSQLRibbonTabParameters;
  30052. TabParametersCount, TabParametersSize: integer;
  30053. const NonVisibleTables: array of TSQLRecordClass;
  30054. Actions, Events: PTypeInfo; const aRoot: RawUTF8);
  30055. var i: integer;
  30056. Tables: array of TSQLRecordClass;
  30057. begin
  30058. if (TabParameters=nil) or (TabParametersCount<=0) or
  30059. (cardinal(TabParametersSize)<sizeof(TSQLRibbonTabParameters)) then
  30060. raise EModelException.CreateUTF8('%.Create(TabParameters?)',[self]);
  30061. SetLength(Tables,TabParametersCount+length(NonVisibleTables));
  30062. for i := 0 to TabParametersCount-1 do begin
  30063. Tables[i] := TabParameters^.Table;
  30064. inc(PByte(TabParameters),TabParametersSize);
  30065. end;
  30066. for i := 0 to high(NonVisibleTables) do
  30067. Tables[i+TabParametersCount] := NonVisibleTables[i];
  30068. Create(Tables,aRoot);
  30069. fRestOwner := Owner;
  30070. SetActions(Actions);
  30071. SetEvents(Events);
  30072. end;
  30073. constructor TSQLModel.Create;
  30074. begin
  30075. raise EModelException.CreateUTF8('Plain %.Create is not allowed: use overloaded Create()',[self]);
  30076. end;
  30077. constructor TSQLModel.Create(const Tables: array of TSQLRecordClass; const aRoot: RawUTF8);
  30078. var N, i: integer;
  30079. begin
  30080. N := length(Tables);
  30081. if N>sizeof(SUPERVISOR_ACCESS_RIGHTS.Get)*8 then // TSQLAccessRights bits size
  30082. raise EModelException.CreateUTF8('% for "%" has too many Tables: %>%',
  30083. [self,aRoot,N,sizeof(SUPERVISOR_ACCESS_RIGHTS.Get)*8]); // e.g. N>64
  30084. // set the Tables to be associated with this Model, as TSQLRecord classes
  30085. fTablesMax := N-1;
  30086. SetLength(fTables,N);
  30087. MoveFast(Tables[0],fTables[0],N*Sizeof(Tables[0]));
  30088. for i := 0 to N-1 do
  30089. // first register for JSONToObject() and for TSQLPropInfoRTTITID.Create()
  30090. TJSONSerializer.RegisterClassForJSON(Tables[i]);
  30091. SetLength(fSortedTablesName,N);
  30092. SetLength(fSortedTablesNameIndex,N);
  30093. SetLength(fTableProps,N);
  30094. // initialize internal properties
  30095. for i := 0 to fTablesMax do
  30096. SetTableProps(i);
  30097. QuickSortRawUTF8(fSortedTablesName,fTablesMax+1,@fSortedTablesNameIndex,@StrIComp);
  30098. // set the optional Root URI path of this Model
  30099. if aRoot<>'' then
  30100. if aRoot[length(aRoot)]='/' then
  30101. fRoot := copy(aRoot,1,Length(aRoot)-1) else
  30102. fRoot := aRoot;
  30103. end;
  30104. function TSQLModel.GetIsUnique(aTable: TSQLRecordClass; aFieldIndex: integer): boolean;
  30105. var i: integer;
  30106. begin
  30107. i := GetTableIndex(aTable);
  30108. if (i<0) or (Cardinal(aFieldIndex)>=MAX_SQLFIELDS) then
  30109. Result := false else
  30110. Result := aFieldIndex in TableProps[i].Props.IsUniqueFieldsBits;
  30111. end;
  30112. function GetTableNameFromSQLSelect(const SQL: RawUTF8;
  30113. EnsureUniqueTableInFrom: boolean): RawUTF8;
  30114. var i,j,k: integer;
  30115. begin
  30116. i := PosI(' FROM ',SQL);
  30117. if i>0 then begin
  30118. inc(i,6);
  30119. while SQL[i] in [#1..' '] do inc(i);
  30120. j := 0;
  30121. while ord(SQL[i+j]) in IsIdentifier do inc(j);
  30122. if cardinal(j-1)<64 then begin
  30123. k := i+j;
  30124. while SQL[k] in [#1..' '] do inc(k);
  30125. if (not EnsureUniqueTableInFrom) or (SQL[k]<>',') then begin
  30126. SetString(result,PAnsiChar(PtrInt(SQL)+i-1),j);
  30127. exit;
  30128. end;
  30129. end;
  30130. end;
  30131. result := '';
  30132. end;
  30133. function GetTableNamesFromSQLSelect(const SQL: RawUTF8): TRawUTF8DynArray;
  30134. var i,j,k,n: integer;
  30135. begin
  30136. result := nil;
  30137. n := 0;
  30138. i := PosI(' FROM ',SQL);
  30139. if i>0 then begin
  30140. inc(i,6);
  30141. repeat
  30142. while SQL[i] in [#1..' '] do inc(i);
  30143. j := 0;
  30144. while ord(SQL[i+j]) in IsIdentifier do inc(j);
  30145. if cardinal(j-1)>64 then begin
  30146. result := nil;
  30147. exit; // seems too big
  30148. end;
  30149. k := i+j;
  30150. while SQL[k] in [#1..' '] do inc(k);
  30151. SetLength(result,n+1);
  30152. SetString(result[n],PAnsiChar(PtrInt(SQL)+i-1),j);
  30153. inc(n);
  30154. if SQL[k]<>',' then
  30155. break;
  30156. i := k+1;
  30157. until false;
  30158. end;
  30159. end;
  30160. function TSQLModel.GetTableIndexFromSQLSelect(const SQL: RawUTF8;
  30161. EnsureUniqueTableInFrom: boolean): integer;
  30162. var TableName: RawUTF8;
  30163. begin
  30164. TableName := GetTableNameFromSQLSelect(SQL,EnsureUniqueTableInFrom);
  30165. result := GetTableIndex(TableName);
  30166. end;
  30167. function TSQLModel.GetTableIndexesFromSQLSelect(const SQL: RawUTF8): TIntegerDynArray;
  30168. var TableNames: TRawUTF8DynArray;
  30169. i,t,n,ndx: integer;
  30170. begin
  30171. result := nil;
  30172. TableNames := GetTableNamesFromSQLSelect(SQL);
  30173. t := length(TableNames);
  30174. if t=0 then
  30175. exit;
  30176. SetLength(result,t);
  30177. n := 0;
  30178. for i := 0 to t-1 do begin
  30179. ndx := GetTableIndex(TableNames[i]);
  30180. if ndx<0 then
  30181. continue;
  30182. result[n] := ndx;
  30183. inc(n);
  30184. end;
  30185. if n<>t then
  30186. SetLength(result,n);
  30187. end;
  30188. function TSQLModel.GetTablesFromSQLSelect(const SQL: RawUTF8): TSQLRecordClassDynArray;
  30189. var t: TIntegerDynArray;
  30190. n,i: integer;
  30191. begin
  30192. t := GetTableIndexesFromSQLSelect(SQL);
  30193. n := length(t);
  30194. if n=0 then
  30195. exit;
  30196. SetLength(result,n);
  30197. for i := 0 to n-1 do
  30198. result[i] := Tables[t[i]];
  30199. end;
  30200. function TSQLModel.GetTable(const SQLTableName: RawUTF8): TSQLRecordClass;
  30201. var i: integer;
  30202. begin
  30203. i := GetTableIndex(SQLTableName);
  30204. if i>=0 then
  30205. result := Tables[i] else
  30206. result := nil;
  30207. end;
  30208. function TSQLModel.GetTableExactClass(const TableName: RawUTF8): TSQLRecordClass;
  30209. var i: integer;
  30210. begin
  30211. i := GetTableExactIndex(TableName);
  30212. if i>=0 then
  30213. result := Tables[i] else
  30214. result := nil;
  30215. end;
  30216. function TSQLModel.GetTableIndex(aTable: TSQLRecordClass): integer;
  30217. var i: integer;
  30218. Props: TSQLRecordProperties;
  30219. begin
  30220. if (self<>nil) and (aTable<>nil) then begin
  30221. Props := PPointer(PtrInt(aTable)+vmtAutoTable)^;
  30222. if (Props<>nil) and (Props.fModelMax>=0) and (Props.fModelMax<fTablesMax) then
  30223. // fastest O(1) search in all registered models (if worth it)
  30224. for i := 0 to Props.fModelMax do
  30225. if Props.fModel[i].Model=self then begin
  30226. result := Props.fModel[i].TableIndex;
  30227. exit;
  30228. end;
  30229. // manual search e.g. if fModel[] is not yet set
  30230. for result := 0 to fTablesMax do
  30231. if Tables[result]=aTable then
  30232. exit;
  30233. end;
  30234. result := -1;
  30235. end;
  30236. function TSQLModel.GetTableIndexInheritsFrom(aTable: TSQLRecordClass): integer;
  30237. begin
  30238. if (self<>nil) and (aTable<>nil) and (aTable<>TSQLRecord) then
  30239. for result := 0 to fTablesMax do
  30240. if Tables[result].InheritsFrom(aTable) then
  30241. exit;
  30242. result := -1;
  30243. end;
  30244. function TSQLModel.GetTableIndexExisting(aTable: TSQLRecordClass): integer;
  30245. begin
  30246. if self=nil then
  30247. raise EModelException.Create('nil.GetTableIndexExisting');
  30248. if aTable=nil then
  30249. raise EModelException.CreateUTF8('aTable=nil for % "%"',[self,Root]);
  30250. result := GetTableIndex(aTable);
  30251. if result<0 then
  30252. raise EModelException.CreateUTF8('% should be part of the % "%"',
  30253. [aTable,self,Root]);
  30254. end;
  30255. function TSQLModel.GetTableExactIndex(const TableName: RawUTF8): integer;
  30256. var L: integer;
  30257. begin
  30258. if self<>nil then begin
  30259. L := length(TableName);
  30260. for result := 0 to fTablesMax do
  30261. if Tables[result]<>nil then // avoid GPF
  30262. if IdemPropName(
  30263. // new TObject.ClassName is UnicodeString (Delphi 20009) -> inline code
  30264. // using vmtClassName = UTF-8 encoded text stored as shortstring
  30265. PShortString(PPointer(PtrInt(Tables[result])+vmtClassName)^)^,
  30266. pointer(TableName),L) then
  30267. exit; // case insensitive search
  30268. end;
  30269. result := -1;
  30270. end;
  30271. function TSQLModel.GetTableIndex(const SQLTableName: RawUTF8): integer;
  30272. begin
  30273. if (self<>nil) and (SQLTableName<>'') then begin
  30274. // fast binary search
  30275. result := FastFindPUTF8CharSorted(
  30276. pointer(fSortedTablesName),fTablesMax,pointer(SQLTableName),@StrIComp);
  30277. if result>=0 then
  30278. result := fSortedTablesNameIndex[result];
  30279. end else
  30280. result := -1;
  30281. end;
  30282. function TSQLModel.GetTableIndex(SQLTableName: PUTF8Char): integer;
  30283. begin
  30284. if (self<>nil) and (SQLTableName<>nil) then begin
  30285. // fast binary search
  30286. result := FastFindPUTF8CharSorted(
  30287. pointer(fSortedTablesName),fTablesMax,SQLTableName,@StrIComp);
  30288. if result>=0 then
  30289. result := fSortedTablesNameIndex[result];
  30290. end else
  30291. result := -1;
  30292. end;
  30293. function TSQLModel.getURI(aTable: TSQLRecordClass): RawUTF8;
  30294. begin
  30295. result := '';
  30296. if self=nil then
  30297. exit;
  30298. if aTable<>nil then
  30299. result := aTable.RecordProps.SQLTableName else begin
  30300. result := Root;
  30301. exit;
  30302. end;
  30303. if Root<>'' then
  30304. result := Root+'/'+result;
  30305. end;
  30306. function TSQLModel.URIMatch(const URI: RawUTF8): TSQLRestModelMatch;
  30307. var URILen: integer;
  30308. begin
  30309. result := rmNoMatch;
  30310. if (self=nil) or (fRoot='') or (URI='') then
  30311. exit;
  30312. if fRootUpper='' then
  30313. UpperCaseCopy(fRoot,fRootUpper);
  30314. if IdemPChar(pointer(URI),pointer(fRootUpper)) then begin
  30315. URILen := length(fRoot);
  30316. if URI[URILen+1] in [#0,'/','?'] then
  30317. if CompareMem(pointer(URI),pointer(fRoot),URILen) then
  30318. result := rmMatchExact else
  30319. result := rmMatchWithCaseChange;
  30320. end;
  30321. end;
  30322. function TSQLModel.SQLFromSelectWhere(const Tables: array of TSQLRecordClass;
  30323. const SQLSelect, SQLWhere: RawUTF8): RawUTF8;
  30324. var i: integer;
  30325. aProps: array[0..31] of TSQLModelRecordProperties;
  30326. begin
  30327. if self=nil then
  30328. raise EORMException.Create('Model required');
  30329. if high(Tables)=0 then begin
  30330. // fastest common call with one TSQLRecordClass
  30331. result := Props[Tables[0]].SQLFromSelectWhere(SQLSelect,SQLWhere);
  30332. exit;
  30333. end;
  30334. // 'SELECT T1.F1,T1.F2,T1.F3,T2.F1,T2.F2 FROM T1,T2 WHERE ..' e.g.
  30335. if cardinal(high(Tables))>high(aProps) then
  30336. raise EModelException.CreateUTF8('%.SQLFromSelectWhere() up to % Tables[]',
  30337. [self,Length(aProps)]);
  30338. for i := 0 to high(Tables) do
  30339. aProps[i] := Props[Tables[i]]; // raise EModelException if not found
  30340. if SQLSelect='*' then
  30341. // don't send BLOB values to query: retrieve all other fields
  30342. if high(Tables)=0 then
  30343. result := 'SELECT '+aProps[0].SQL.TableSimpleFields[true,false] else begin
  30344. result := 'SELECT '+aProps[0].SQL.TableSimpleFields[true,true];
  30345. for i := 1 to high(Tables) do
  30346. result := result+','+aProps[i].SQL.TableSimpleFields[true,true];
  30347. end else
  30348. result := 'SELECT '+SQLSelect;
  30349. result := result+' FROM '+aProps[0].Props.SQLTableName;
  30350. for i := 1 to high(Tables) do
  30351. result := result+','+aProps[i].Props.SQLTableName;
  30352. result := result+SQLFromWhere(SQLWhere);
  30353. end;
  30354. procedure TSQLModel.SetCustomCollationForAll(aFieldType: TSQLFieldType;
  30355. const aCollationName: RawUTF8);
  30356. var i: integer;
  30357. begin
  30358. if self=nil then
  30359. exit;
  30360. if fCustomCollationForAll[aFieldType]<>'' then
  30361. raise EModelException.CreateUTF8('%.SetCustomCollationForAll(%)'+
  30362. ' shall be called only once',[self,aCollationName]);
  30363. fCustomCollationForAll[aFieldType] := aCollationName;
  30364. for i := 0 to high(fTableProps) do
  30365. fTableProps[i].fProps.SetCustomCollationForAll(aFieldType,aCollationName);
  30366. end;
  30367. procedure TSQLModel.SetMaxLengthValidatorForAllTextFields(IndexIsUTF8Length: boolean);
  30368. var i: integer;
  30369. begin
  30370. if self<>nil then
  30371. for i := 0 to high(fTableProps) do
  30372. fTableProps[i].fProps.SetMaxLengthValidatorForTextFields(IndexIsUTF8Length);
  30373. end;
  30374. procedure TSQLModel.SetMaxLengthFilterForAllTextFields(IndexIsUTF8Length: boolean);
  30375. var i: integer;
  30376. begin
  30377. if self<>nil then
  30378. for i := 0 to high(fTableProps) do
  30379. fTableProps[i].fProps.SetMaxLengthFilterForTextFields(IndexIsUTF8Length);
  30380. end;
  30381. {$ifndef NOVARIANTS}
  30382. procedure TSQLModel.SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions);
  30383. var i: integer;
  30384. begin
  30385. if self<>nil then
  30386. for i := 0 to high(fTableProps) do
  30387. fTableProps[i].fProps.SetVariantFieldsDocVariantOptions(Options);
  30388. end;
  30389. {$endif}
  30390. function TSQLModel.SetIDGenerator(aTable: TSQLRecordClass;
  30391. aIdentifier: TSynUniqueIdentifierProcess; const aSharedObfuscationKey: RawUTF8): TSynUniqueIdentifierGenerator;
  30392. var i: integer;
  30393. begin
  30394. i := GetTableIndexExisting(aTable);
  30395. if i>=length(fIDGenerator) then
  30396. SetLength(fIDGenerator,fTablesMax+1);
  30397. result := TSynUniqueIdentifierGenerator.Create(aIdentifier,aSharedObfuscationKey);
  30398. fIDGenerator[i].Free;
  30399. fIDGenerator[i] := result;
  30400. end;
  30401. function TSQLModel.GetIDGenerator(aTable: TSQLRecordClass): TSynUniqueIdentifierGenerator;
  30402. var i: cardinal;
  30403. begin
  30404. i := GetTableIndexExisting(aTable);
  30405. if i<cardinal(length(fIDGenerator)) then
  30406. result := fIDGenerator[i] else
  30407. result := nil;
  30408. end;
  30409. function TSQLModel.NewRecord(const SQLTableName: RawUTF8): TSQLRecord;
  30410. var aClass: TSQLRecordClass;
  30411. begin
  30412. aClass := Table[SQLTableName];
  30413. if aClass=nil then
  30414. result := nil else
  30415. result := aClass.Create;
  30416. end;
  30417. procedure TSQLModel.SetActions(aActions: PTypeInfo);
  30418. begin
  30419. if (aActions=nil) or not (aActions^.Kind=tkEnumeration) then
  30420. fActions := nil else
  30421. fActions := aActions^.EnumBaseType;
  30422. end;
  30423. procedure TSQLModel.SetEvents(aEvents: PTypeInfo);
  30424. begin
  30425. if (aEvents=nil) or not (aEvents^.Kind=tkEnumeration) then
  30426. fEvents := nil else
  30427. fEvents := aEvents^.EnumBaseType;
  30428. end;
  30429. function TSQLModel.GetSQLCreate(aTableIndex: integer): RawUTF8;
  30430. begin
  30431. if (self=nil) or (cardinal(aTableIndex)>cardinal(fTablesMax)) then
  30432. result := '' else
  30433. result := Tables[aTableIndex].GetSQLCreate(self);
  30434. end;
  30435. function TSQLModel.GetSQLAddField(aTableIndex, aFieldIndex: integer): RawUTF8;
  30436. begin
  30437. if (self=nil) or (cardinal(aTableIndex)>cardinal(fTablesMax)) then
  30438. result := '' else
  30439. result := TableProps[aTableIndex].Props.SQLAddField(aFieldIndex);
  30440. end;
  30441. function TSQLModel.isLocked(aTable: TSQLRecordClass; aID: TID): boolean;
  30442. begin
  30443. result := GetLocks(aTable)^.isLocked(aID);
  30444. end;
  30445. function TSQLModel.isLocked(aRec: TSQLRecord): boolean;
  30446. begin
  30447. if aRec=nil then
  30448. result := false else
  30449. result := isLocked(PSQLRecordClass(aRec)^,aRec.fID);
  30450. end;
  30451. function TSQLModel.Lock(aTable: TSQLRecordClass; aID: TID): boolean;
  30452. begin
  30453. if self=nil then
  30454. result := false else begin
  30455. if fLocks=nil then
  30456. SetLength(fLocks,fTablesMax+1); // initialize fLocks[] if necessary
  30457. result := GetLocks(aTable)^.Lock(aID);
  30458. end;
  30459. end;
  30460. function TSQLModel.Lock(aTableIndex, aID: TID): boolean;
  30461. begin
  30462. if (self=nil) or (Cardinal(aTableIndex)>cardinal(fTablesMax)) then
  30463. result := false else begin
  30464. if fLocks=nil then
  30465. SetLength(fLocks,fTablesMax+1); // initialize fLocks[] if necessary
  30466. result := fLocks[aTableIndex].Lock(aID);
  30467. end;
  30468. end;
  30469. function TSQLModel.Lock(aRec: TSQLRecord): boolean;
  30470. begin
  30471. if aRec=nil then
  30472. result := false else
  30473. result := Lock(PSQLRecordClass(aRec)^,aRec.fID);
  30474. end;
  30475. procedure TSQLModel.PurgeOlderThan(MinutesFromNow: cardinal);
  30476. var i: integer;
  30477. begin
  30478. if fLocks<>nil then
  30479. for i := 0 to high(fLocks) do
  30480. fLocks[i].PurgeOlderThan(MinutesFromNow);
  30481. end;
  30482. function TSQLModel.UnLock(aTable: TSQLRecordClass; aID: TID): boolean;
  30483. begin
  30484. if (self=nil) or (fLocks=nil) then
  30485. result := false else
  30486. result := GetLocks(aTable)^.UnLock(aID);
  30487. end;
  30488. function TSQLModel.UnLock(aTableIndex: integer; aID: TID): boolean;
  30489. begin
  30490. if (self=nil) or (cardinal(aTableIndex)>=cardinal(length(fLocks))) then
  30491. result := false else
  30492. result := fLocks[aTableIndex].UnLock(aID);
  30493. end;
  30494. function TSQLModel.UnLock(aRec: TSQLRecord): boolean;
  30495. begin
  30496. if aRec=nil then
  30497. result := false else
  30498. result := UnLock(PSQLRecordClass(aRec)^,aRec.fID);
  30499. end;
  30500. function TSQLModel.GetLocks(aTable: TSQLRecordClass): PSQLLocks;
  30501. begin
  30502. if (self=nil) or (fLocks=nil) then
  30503. result := nil else
  30504. result := @fLocks[GetTableIndexExisting(aTable)];
  30505. end;
  30506. procedure TSQLModel.UnLockAll;
  30507. var i: integer;
  30508. begin
  30509. for i := 0 to high(fLocks) do
  30510. fLocks[i].Count := 0;
  30511. end;
  30512. function TSQLModel.getURIID(aTable: TSQLRecordClass; aID: TID): RawUTF8;
  30513. begin
  30514. result := getURI(aTable);
  30515. if aID>0 then
  30516. result := result+'/'+Int64ToUtf8(aID);
  30517. end;
  30518. function TSQLModel.getURICallBack(const aMethodName: RawUTF8; aTable: TSQLRecordClass; aID: TID): RawUTF8;
  30519. begin
  30520. result := getURIID(aTable,aID)+'/'+aMethodName;
  30521. end;
  30522. function TSQLModel.ActionName(const Action): string;
  30523. begin
  30524. if (Self=nil) or (fActions=nil) then
  30525. result := '' else
  30526. result := TSQLRecord.CaptionNameFromRTTI(fActions^.GetEnumName(byte(Action)));
  30527. end;
  30528. function TSQLModel.EventName(const Event): string;
  30529. begin
  30530. if (Self=nil) or (fEvents=nil) then
  30531. result := '' else
  30532. result := TSQLRecord.CaptionNameFromRTTI(fEvents^.GetEnumName(byte(Event)));
  30533. end;
  30534. function TSQLModel.RecordReference(Table: TSQLRecordClass; ID: TID): TRecordReference;
  30535. begin
  30536. if (self=nil) or (ID<=0) then
  30537. result := 0 else begin
  30538. result := GetTableIndexExisting(Table);
  30539. if result>63 then // TRecordReference handle up to 64=1 shl 6 tables
  30540. result := 0 else
  30541. inc(result,ID shl 6);
  30542. end;
  30543. end;
  30544. function TSQLModel.RecordReferenceTable(const Ref: TRecordReference): TSQLRecordClass;
  30545. var i: integer;
  30546. begin
  30547. i := Ref and 63;
  30548. if i<=fTablesMax then
  30549. result := fTables[i] else
  30550. result := nil;
  30551. end;
  30552. function TSQLModel.VirtualTableRegister(aClass: TSQLRecordClass;
  30553. aModule: TSQLVirtualTableClass; const aExternalTableName: RawUTF8='';
  30554. aExternalDataBase: TObject=nil): boolean;
  30555. var i: integer;
  30556. begin
  30557. result := false;
  30558. if aClass=nil then exit;
  30559. i := GetTableIndexExisting(aClass);
  30560. with TableProps[i] do begin
  30561. if not (Kind in IS_CUSTOM_VIRTUAL) then
  30562. if Kind=rSQLite3 then
  30563. SetKind(rCustomAutoID) else // SetKind() recompute all SQL
  30564. raise EModelException.CreateUTF8('Invalid %.VirtualTableRegister(%) call: '+
  30565. 'impossible to set class as virtual',[self,aClass]);
  30566. ExternalDB.Init(aClass,aExternalTableName,aExternalDataBase,true);
  30567. end;
  30568. if high(fVirtualTableModule)<>fTablesMax then
  30569. SetLength(fVirtualTableModule,fTablesMax+1);
  30570. fVirtualTableModule[i] := aModule;
  30571. result := true;
  30572. end;
  30573. function TSQLModel.VirtualTableModule(aClass: TSQLRecordClass): TSQLVirtualTableClass;
  30574. var i: integer;
  30575. begin
  30576. result := nil;
  30577. if (self=nil) or (fVirtualTableModule=nil) then
  30578. exit;
  30579. i := GetTableIndexExisting(aClass);
  30580. if TableProps[i].Kind in IS_CUSTOM_VIRTUAL then
  30581. result := fVirtualTableModule[i];
  30582. end;
  30583. destructor TSQLModel.Destroy;
  30584. var i,j: integer;
  30585. begin
  30586. for i := 0 to fTablesMax do begin
  30587. with TableProps[i].Props do begin
  30588. EnterCriticalSection(fLock); // may be called from several threads at once
  30589. try
  30590. for j := 0 to fModelMax do
  30591. if fModel[j].Model=self then begin
  30592. // un-associate this TSQLRecord with this model
  30593. MoveFast(fModel[j+1],fModel[j],(fModelMax-j)*sizeof(fModel[j]));
  30594. dec(fModelMax);
  30595. break;
  30596. end;
  30597. TableProps[i].Free;
  30598. finally
  30599. LeaveCriticalSection(fLock);
  30600. end;
  30601. end;
  30602. end;
  30603. ObjArrayClear(fIDGenerator);
  30604. inherited;
  30605. end;
  30606. { TSQLRestBatch }
  30607. constructor TSQLRestBatch.Create(aRest: TSQLRest; aTable: TSQLRecordClass;
  30608. AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions);
  30609. begin
  30610. if aRest=nil then
  30611. raise EORMException.CreateUTF8('%.Create(aRest=nil)',[self]);
  30612. fRest := aRest;
  30613. Reset(aTable,AutomaticTransactionPerRow,Options);
  30614. end;
  30615. procedure TSQLRestBatch.Reset(aTable: TSQLRecordClass;
  30616. AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions);
  30617. begin
  30618. fBatch.Free; // full reset for SetExpandedJSONWriter
  30619. fBatch := TJSONSerializer.CreateOwnedStream;
  30620. fBatch.Expand := true;
  30621. FillZero(fBatchFields);
  30622. fBatchCount := 0;
  30623. fAddCount := 0;
  30624. fUpdateCount := 0;
  30625. fDeleteCount := 0;
  30626. fDeletedCount := 0;
  30627. fTable := aTable;
  30628. if aTable<>nil then begin
  30629. fTableIndex := fRest.Model.GetTableIndexExisting(aTable);
  30630. fBatch.Add('{'); // sending data is '{"Table":["cmd":values,...]}'
  30631. fBatch.AddFieldName(aTable.SQLTableName);
  30632. end else
  30633. fTableIndex := -1;
  30634. fBatch.Add('[');
  30635. fAutomaticTransactionPerRow := AutomaticTransactionPerRow;
  30636. if AutomaticTransactionPerRow>0 then begin // should be the first command
  30637. fBatch.AddShort('"automaticTransactionPerRow",');
  30638. fBatch.Add(AutomaticTransactionPerRow);
  30639. fBatch.Add(',');
  30640. end;
  30641. fOptions := Options;
  30642. if boExtendedJSON in Options then
  30643. include(fBatch.fCustomOptions,twoForceJSONExtended);
  30644. Options := Options-[boExtendedJSON,boPostNoSimpleFields]; // client-side only
  30645. if byte(Options)<>0 then begin
  30646. fBatch.AddShort('"options",');
  30647. fBatch.Add(byte(Options));
  30648. fBatch.Add(',');
  30649. end;
  30650. end;
  30651. procedure TSQLRestBatch.Reset;
  30652. begin
  30653. if self<>nil then
  30654. Reset(fTable,fAutomaticTransactionPerRow,fOptions);
  30655. end;
  30656. destructor TSQLRestBatch.Destroy;
  30657. begin
  30658. FreeAndNil(fBatch);
  30659. inherited;
  30660. end;
  30661. function TSQLRestBatch.GetCount: integer;
  30662. begin
  30663. if self=nil then
  30664. result := 0 else
  30665. result := fBatchCount;
  30666. end;
  30667. function TSQLRestBatch.GetSizeBytes: cardinal;
  30668. begin
  30669. if self=nil then
  30670. result := 0 else
  30671. result := fBatch.TextLength;
  30672. end;
  30673. procedure TSQLRestBatch.SetExpandedJSONWriter(Props: TSQLRecordProperties;
  30674. ForceResetFields, withID: boolean; const WrittenFields: TSQLFieldBits);
  30675. begin
  30676. if (self=nil) or (fBatch=nil) then
  30677. exit;
  30678. if not ForceResetFields then
  30679. if fBatch.Expand and (fBatch.WithID=withID) and
  30680. IsEqual(fBatchFields,WrittenFields) then
  30681. exit; // already set -> do not compute it again
  30682. fBatchFields := WrittenFields;
  30683. fBatch.ChangeExpandedFields(withID,FieldBitsToIndex(WrittenFields,Props.Fields.Count));
  30684. Props.SetJSONWriterColumnNames(fBatch,0);
  30685. end;
  30686. function TSQLRestBatch.RawAppend(FullRow: boolean): TTextWriter;
  30687. begin
  30688. if FullRow then
  30689. inc(fBatchCount);
  30690. result := fBatch;
  30691. end;
  30692. procedure TSQLRestBatch.RawAdd(const SentData: RawUTF8);
  30693. begin // '{"Table":[...,"POST",{object},...]}'
  30694. if (fBatch=nil) or (fTable=nil) then
  30695. raise EORMException.CreateUTF8('%.RawAdd %',[self,SentData]);
  30696. fBatch.AddShort('"POST",');
  30697. fBatch.AddString(SentData);
  30698. fBatch.Add(',');
  30699. inc(fBatchCount);
  30700. inc(fAddCount);
  30701. end;
  30702. procedure TSQLRestBatch.RawUpdate(const SentData: RawUTF8; ID: TID);
  30703. var sentID: TID;
  30704. begin // '{"Table":[...,"PUT",{object},...]}'
  30705. if (fBatch=nil) or (fTable=nil) then
  30706. raise EORMException.CreateUTF8('%.RawUpdate % %',[self,ID,SentData]);
  30707. if JSONGetID(pointer(SentData),sentID) and (sentID<>ID) then
  30708. raise EORMException.CreateUTF8('%.RawUpdate ID=% <> %',[self,ID,SentData]);
  30709. fBatch.AddShort('"PUT",{ID:');
  30710. fBatch.Add(ID);
  30711. fBatch.Add(',');
  30712. fBatch.AddStringCopy(SentData,2,maxInt shr 2);
  30713. fBatch.Add(',');
  30714. inc(fBatchCount);
  30715. inc(fUpdateCount);
  30716. end;
  30717. function TSQLRestBatch.Add(Value: TSQLRecord; SendData,ForceID: boolean;
  30718. const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer;
  30719. var Props: TSQLRecordProperties;
  30720. FieldBits: TSQLFieldBits;
  30721. PostSimpleFields: boolean;
  30722. f: integer;
  30723. begin
  30724. result := -1;
  30725. if (self=nil) or (Value=nil) or (fBatch=nil) then
  30726. exit; // invalid parameters, or not opened BATCH sequence
  30727. if (fTable<>nil) and (PSQLRecordClass(Value)^<>fTable) then
  30728. exit;
  30729. Props := Value.RecordProps;
  30730. if SendData and
  30731. (fRest.Model.Props[PSQLRecordClass(Value)^].Kind in INSERT_WITH_ID) then
  30732. ForceID := true; // same format as TSQLRestClient.Add
  30733. if SendData and (not ForceID) and IsZero(CustomFields) and
  30734. not(boPostNoSimpleFields in fOptions) then begin
  30735. PostSimpleFields := true;
  30736. fBatch.AddShort('"SIMPLE');
  30737. end else begin
  30738. PostSimpleFields := false;
  30739. fBatch.AddShort('"POST');
  30740. end;
  30741. if fTable<>nil then // '{"Table":[...,"POST",{object},...]}'
  30742. fBatch.AddShort('",') else begin
  30743. fBatch.Add('@'); // '[...,"POST@Table",{object}',...]'
  30744. fBatch.AddString(Props.SQLTableName);
  30745. fBatch.Add('"',',');
  30746. end;
  30747. if SendData then begin
  30748. if IsZero(CustomFields) then
  30749. FieldBits := Props.SimpleFieldsBits[soInsert] else
  30750. if DoNotAutoComputeFields then
  30751. FieldBits := CustomFields else
  30752. FieldBits := CustomFields+Props.ComputeBeforeAddFieldsBits;
  30753. SetExpandedJSONWriter(Props,fTablePreviousSendData<>PSQLRecordClass(Value)^,
  30754. (Value.IDValue<>0) and ForceID,FieldBits);
  30755. fTablePreviousSendData := PSQLRecordClass(Value)^;
  30756. if not DoNotAutoComputeFields then // update TModTime/TCreateTime fields
  30757. Value.ComputeFieldsBeforeWrite(fRest,seAdd);
  30758. if PostSimpleFields then begin
  30759. fBatch.Add('[');
  30760. for f := 0 to length(Props.SimpleFields)-1 do begin
  30761. Props.SimpleFields[f].GetJSONValues(Value,fBatch);
  30762. fBatch.Add(',');
  30763. end;
  30764. fBatch.CancelLastComma;
  30765. fBatch.Add(']');
  30766. end else
  30767. Value.GetJSONValues(fBatch);
  30768. if fCalledWithinRest and ForceID then
  30769. fRest.fCache.Notify(Value,soInsert);
  30770. end else
  30771. fBatch.Add('{','}'); // '{"Table":[...,"POST",{},...]}'
  30772. fBatch.Add(',');
  30773. result := fBatchCount;
  30774. inc(fBatchCount);
  30775. inc(fAddCount);
  30776. if Assigned(fOnWrite) then
  30777. fOnWrite(self,soInsert,PSQLRecordClass(Value)^,Value.IDValue,Value,FieldBits);
  30778. end;
  30779. procedure AddID(var Values: TIDDynArray; var ValuesCount: integer; Value: TID);
  30780. begin
  30781. if ValuesCount=length(Values) then
  30782. SetLength(Values,ValuesCount+256+ValuesCount shr 3);
  30783. Values[ValuesCount] := Value;
  30784. inc(ValuesCount);
  30785. end;
  30786. function TSQLRestBatch.Delete(Table: TSQLRecordClass;
  30787. ID: TID): integer;
  30788. begin
  30789. if (self=nil) or (fBatch=nil) or (Table=nil) or
  30790. (ID<=0) or not fRest.RecordCanBeUpdated(Table,ID,seDelete) then begin
  30791. result := -1; // invalid parameters, or not opened BATCH sequence
  30792. exit;
  30793. end;
  30794. AddID(fDeletedRecordRef,fDeletedCount,fRest.Model.RecordReference(Table,ID));
  30795. fBatch.AddShort('"DELETE@'); // '[...,"DELETE@Table",ID,...]}'
  30796. fBatch.AddString(Table.RecordProps.SQLTableName);
  30797. fBatch.Add('"',',');
  30798. fBatch.Add(ID);
  30799. fBatch.Add(',');
  30800. result := fBatchCount;
  30801. inc(fBatchCount);
  30802. inc(fDeleteCount);
  30803. if Assigned(fOnWrite) then
  30804. fOnWrite(self,soDelete,Table,ID,nil,[]);
  30805. end;
  30806. function TSQLRestBatch.Delete(ID: TID): integer;
  30807. begin
  30808. if (self=nil) or (fTable=nil) or
  30809. (ID<=0) or not fRest.RecordCanBeUpdated(fTable,ID,seDelete) then begin
  30810. result := -1; // invalid parameters, or not opened BATCH sequence
  30811. exit;
  30812. end;
  30813. AddID(fDeletedRecordRef,fDeletedCount,RecordReference(fTableIndex,ID));
  30814. fBatch.AddShort('"DELETE",'); // '{"Table":[...,"DELETE",ID,...]}'
  30815. fBatch.Add(ID);
  30816. fBatch.Add(',');
  30817. result := fBatchCount;
  30818. inc(fBatchCount);
  30819. inc(fDeleteCount);
  30820. if Assigned(fOnWrite) then
  30821. fOnWrite(self,soDelete,fTable,ID,nil,[]);
  30822. end;
  30823. function TSQLRestBatch.PrepareForSending(out Data: RawUTF8): boolean;
  30824. var i: integer;
  30825. begin
  30826. if (self=nil) or (fBatch=nil) then // no opened BATCH sequence
  30827. result := false else begin
  30828. if fBatchCount>0 then begin // if something to send
  30829. for i := 0 to fDeletedCount-1 do
  30830. if fDeletedRecordRef[i]<>0 then
  30831. fRest.Cache.NotifyDeletion(fDeletedRecordRef[i] and 63,fDeletedRecordRef[i] shr 6);
  30832. fBatch.CancelLastComma;
  30833. fBatch.Add(']');
  30834. if fTable<>nil then
  30835. fBatch.Add('}'); // end sequence array '{"Table":["cmd":values,...]}'
  30836. fBatch.SetText(Data);
  30837. end;
  30838. result := true;
  30839. end;
  30840. end;
  30841. function TSQLRestBatch.Update(Value: TSQLRecord;
  30842. const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer;
  30843. var Props: TSQLRecordProperties;
  30844. FieldBits: TSQLFieldBits;
  30845. ID: TID;
  30846. tableIndex: integer;
  30847. begin
  30848. result := -1;
  30849. if (Value=nil) or (fBatch=nil) then
  30850. exit;
  30851. ID := Value.IDValue;
  30852. if (ID<=0) or not fRest.RecordCanBeUpdated(Value.RecordClass,ID,seUpdate) then
  30853. exit; // invalid parameters, or not opened BATCH sequence
  30854. Props := Value.RecordProps;
  30855. if fTable<>nil then
  30856. if PSQLRecordClass(Value)^<>fTable then
  30857. exit else begin // '{"Table":[...,"PUT",{object},...]}'
  30858. tableIndex := fTableIndex;
  30859. fBatch.AddShort('"PUT",');
  30860. end else begin
  30861. tableIndex := fRest.Model.GetTableIndexExisting(Props.Table);
  30862. fBatch.AddShort('"PUT@'); // '[...,"PUT@Table",{object}',...]'
  30863. fBatch.AddString(Props.SQLTableName);
  30864. fBatch.Add('"',',');
  30865. end;
  30866. // same format as TSQLRest.Update, BUT including the ID
  30867. if IsZero(CustomFields) then
  30868. Value.FillContext.ComputeSetUpdatedFieldBits(Props,FieldBits) else
  30869. if DoNotAutoComputeFields then
  30870. FieldBits := CustomFields else
  30871. FieldBits := CustomFields+Value.RecordProps.FieldBits[sftModTime];
  30872. SetExpandedJSONWriter(Props,fTablePreviousSendData<>PSQLRecordClass(Value)^,
  30873. true,FieldBits);
  30874. fTablePreviousSendData := PSQLRecordClass(Value)^;
  30875. if not DoNotAutoComputeFields then
  30876. Value.ComputeFieldsBeforeWrite(fRest,seUpdate); // update sftModTime fields
  30877. Value.GetJSONValues(fBatch);
  30878. fBatch.Add(',');
  30879. if fCalledWithinRest and
  30880. (FieldBits-Props.SimpleFieldsBits[soUpdate]=[]) then
  30881. fRest.Cache.Notify(Value,soUpdate) else
  30882. // may not contain all cached fields -> delete from cache
  30883. AddID(fDeletedRecordRef,fDeletedCount,RecordReference(tableIndex,ID));
  30884. result := fBatchCount;
  30885. inc(fBatchCount);
  30886. inc(fUpdateCount);
  30887. if Assigned(fOnWrite) then
  30888. fOnWrite(self,soUpdate,PSQLRecordClass(Value)^,Value.IDValue,Value,FieldBits);
  30889. end;
  30890. function TSQLRestBatch.Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8;
  30891. DoNotAutoComputeFields: boolean): integer;
  30892. begin
  30893. if (Value=nil) or (fBatch=nil) then
  30894. result := -1 else
  30895. result := Update(Value,Value.RecordProps.FieldBitsFromCSV(CustomCSVFields),
  30896. DoNotAutoComputeFields);
  30897. end;
  30898. { TSQLRestBatchLocked }
  30899. constructor TSQLRestBatchLocked.Create(aRest: TSQLRest; aTable: TSQLRecordClass;
  30900. AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions);
  30901. begin
  30902. inherited;
  30903. fSafe.Init;
  30904. end;
  30905. destructor TSQLRestBatchLocked.Destroy;
  30906. begin
  30907. fSafe.Done;
  30908. inherited;
  30909. end;
  30910. procedure TSQLRestBatchLocked.Reset(aTable: TSQLRecordClass;
  30911. AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions);
  30912. begin
  30913. inherited;
  30914. fTix := GetTickCount64;
  30915. end;
  30916. { TSQLRest }
  30917. constructor TSQLRest.Create(aModel: TSQLModel);
  30918. var cmd: TSQLRestServerURIContextCommand;
  30919. begin
  30920. inherited Create;
  30921. fPrivateGarbageCollector := TObjectList.Create;
  30922. fModel := aModel;
  30923. for cmd := Low(cmd) to high(cmd) do
  30924. fAcquireExecution[cmd] := TSQLRestAcquireExecution.Create;
  30925. AcquireWriteMode := amLocked;
  30926. AcquireWriteTimeOut := 2000; // default 2 seconds
  30927. fRoutingClass := TSQLRestRoutingREST;
  30928. QueryPerformanceFrequency(fFrequencyTimeStamp);
  30929. {$ifdef WITHLOG}
  30930. SetLogClass(SQLite3Log); // by default
  30931. {$endif}
  30932. end;
  30933. destructor TSQLRest.Destroy;
  30934. var cmd: TSQLRestServerURIContextCommand;
  30935. i: integer;
  30936. begin
  30937. {$ifndef FPC} // serialization during destruction seems unsafe under FPC
  30938. InternalLog('%.Destroy -> %',[ClassType,self],sllInfo);
  30939. {$endif}
  30940. FreeAndNil(fServices);
  30941. FreeAndNil(fCache);
  30942. if (fModel<>nil) and (fModel.fRestOwner=self) then
  30943. // make sure we are the Owner (TSQLRestStorage has fModel<>nil e.g.)
  30944. FreeAndNil(fModel);
  30945. for cmd := Low(cmd) to high(cmd) do
  30946. FreeAndNil(fAcquireExecution[cmd]); // should be done BEFORE private GC
  30947. if fPrivateGarbageCollector<>nil then begin
  30948. for i := fPrivateGarbageCollector.Count-1 downto 0 do // last in, first out
  30949. try
  30950. fPrivateGarbageCollector.Delete(i); // will call fPrivate...[i].Free
  30951. except
  30952. on Exception do
  30953. ; // just ignore exceptions in such destructors
  30954. end;
  30955. fPrivateGarbageCollector.Free;
  30956. end;
  30957. inherited Destroy;
  30958. end;
  30959. var
  30960. GlobalDefinitions: array of TSQLRestClass;
  30961. class procedure TSQLRest.RegisterClassNameForDefinition;
  30962. begin
  30963. ObjArrayAddOnce(GlobalDefinitions,TObject(self)); // TClass stored as TObject
  30964. end;
  30965. procedure TSQLRest.DefinitionTo(Definition: TSynConnectionDefinition);
  30966. begin
  30967. if Definition<>nil then
  30968. Definition.Kind := ClassName;
  30969. end;
  30970. function TSQLRest.DefinitionToJSON(Key: cardinal=0): RawUTF8;
  30971. var Definition: TSynConnectionDefinition;
  30972. begin
  30973. Definition := TSynConnectionDefinition.Create;
  30974. try
  30975. Definition.Key := Key;
  30976. DefinitionTo(Definition);
  30977. result := Definition.SaveToJSON;
  30978. finally
  30979. Definition.Free;
  30980. end;
  30981. end;
  30982. procedure TSQLRest.DefinitionToFile(const aJSONFile: TFileName; aKey: cardinal);
  30983. begin
  30984. FileFromString(JSONReformat(DefinitionToJSON(aKey)),aJSONFile);
  30985. end;
  30986. class function TSQLRest.ClassFrom(aDefinition: TSynConnectionDefinition): TSQLRestClass;
  30987. var ndx: integer;
  30988. begin
  30989. for ndx := 0 to length(GlobalDefinitions)-1 do
  30990. if GlobalDefinitions[ndx].ClassNameIs(aDefinition.Kind) then begin
  30991. result := GlobalDefinitions[ndx];
  30992. exit;
  30993. end;
  30994. result := nil;
  30995. end;
  30996. constructor TSQLRest.RegisteredClassCreateFrom(aModel: TSQLModel;
  30997. aDefinition: TSynConnectionDefinition);
  30998. begin
  30999. Create(aModel);
  31000. end;
  31001. class function TSQLRest.CreateFrom(aModel: TSQLModel;
  31002. aDefinition: TSynConnectionDefinition): TSQLRest;
  31003. var C: TSQLRestClass;
  31004. begin
  31005. C := ClassFrom(aDefinition);
  31006. if C=nil then
  31007. raise EORMException.CreateUTF8('%.CreateFrom: unknown % class - please '+
  31008. 'add a reference to its implementation unit',[self,aDefinition.Kind]);
  31009. result := C.RegisteredClassCreateFrom(aModel,aDefinition);
  31010. end;
  31011. class function TSQLRest.CreateTryFrom(aModel: TSQLModel;
  31012. aDefinition: TSynConnectionDefinition; aServerHandleAuthentication: boolean): TSQLRest;
  31013. var C: TSQLRestClass;
  31014. begin
  31015. C := ClassFrom(aDefinition);
  31016. if C=nil then
  31017. result := nil else
  31018. if C.InheritsFrom(TSQLRestServer) then
  31019. result := TSQLRestServerClass(C).RegisteredClassCreateFrom(
  31020. aModel,aServerHandleAuthentication,aDefinition) else
  31021. result := C.RegisteredClassCreateFrom(aModel,aDefinition);
  31022. end;
  31023. class function TSQLRest.CreateFromJSON(aModel: TSQLModel;
  31024. const aJSONDefinition: RawUTF8; aKey: cardinal): TSQLRest;
  31025. var Definition: TSynConnectionDefinition;
  31026. begin
  31027. Definition := TSynConnectionDefinition.CreateFromJSON(aJSONDefinition,aKey);
  31028. try
  31029. result := CreateFrom(aModel,Definition);
  31030. finally
  31031. Definition.Free;
  31032. end;
  31033. end;
  31034. class function TSQLRest.CreateFromFile(aModel: TSQLModel;
  31035. const aJSONFile: TFileName; aKey: cardinal): TSQLRest;
  31036. begin
  31037. result := CreateFromJSON(aModel,AnyTextFileToRawUTF8(aJSONFile,true),aKey);
  31038. end;
  31039. procedure TSQLRest.InternalLog(const Text: RawUTF8; Level: TSynLogInfo);
  31040. begin
  31041. {$ifdef WITHLOG}
  31042. if (self<>nil) and (fLogFamily<>nil) and (Level in fLogFamily.Level) then
  31043. fLogFamily.SynLog.Log(Level,Text,self);
  31044. {$endif}
  31045. end;
  31046. procedure TSQLRest.InternalLog(const Format: RawUTF8;
  31047. const Args: array of const; Level: TSynLogInfo);
  31048. begin
  31049. {$ifdef WITHLOG}
  31050. if (self<>nil) and (fLogFamily<>nil) and (Level in fLogFamily.Level) then
  31051. fLogFamily.SynLog.Log(Level,Format,Args,self);
  31052. {$endif}
  31053. end;
  31054. {$ifdef WITHLOG}
  31055. procedure TSQLRest.SetLogClass(aClass: TSynLogClass);
  31056. begin
  31057. fLogClass := aClass;
  31058. fLogFamily := fLogClass.Family;
  31059. end;
  31060. function TSQLRest.GetLogClass: TSynLogClass;
  31061. begin
  31062. if self=nil then
  31063. result := SQLite3Log else
  31064. result := fLogClass;
  31065. end;
  31066. {$endif}
  31067. function TSQLRest.NewBackgroundThreadMethod(const Format: RawUTF8;
  31068. const Args: array of const): TSynBackgroundThreadMethod;
  31069. begin
  31070. result := TSynBackgroundThreadMethod.Create(nil,FormatUTF8(Format,Args),
  31071. BeginCurrentThread,EndCurrentThread);
  31072. end;
  31073. function TSQLRest.NewBackgroundThreadprocess(
  31074. aOnProcess: TOnSynBackgroundThreadProcess; aOnProcessMS: cardinal;
  31075. const Format: RawUTF8; const Args: array of const;
  31076. aStats: TSynMonitorClass): TSynBackgroundThreadProcess;
  31077. var name: RawUTF8;
  31078. begin
  31079. FormatUTF8(Format,Args,name);
  31080. if self=nil then
  31081. result := TSynBackgroundThreadProcess.Create(name,aOnProcess,aOnProcessMS,
  31082. nil,nil,aStats) else
  31083. result := TSynBackgroundThreadProcess.Create(name,aOnProcess,aOnProcessMS,
  31084. BeginCurrentThread,EndCurrentThread,aStats);
  31085. end;
  31086. procedure TSQLRest.AdministrationExecute(const DatabaseName,SQL: RawUTF8;
  31087. var result: TServiceCustomAnswer);
  31088. begin
  31089. if (SQL<>'') and (SQL[1]='#') then begin
  31090. // pseudo SQL for a given TSQLRest[Server] instance
  31091. case IdemPCharArray(@SQL[2],['TIME','MODEL','REST','HELP']) of
  31092. 0: result.Content := Int64ToUtf8(ServerTimeStamp);
  31093. 1: result.Content := ObjectToJSON(Model);
  31094. 2: result.Content := ObjectToJSON(self);
  31095. 3: begin
  31096. result.Content[length(result.Content)] := '|';
  31097. result.Content := result.Content+'#time|#model|#rest"';
  31098. end;
  31099. end;
  31100. end else
  31101. if isSelect(pointer(SQL)) then
  31102. result.Content := ExecuteJson(Model.GetTablesFromSQLSelect(SQL),SQL) else
  31103. Execute(SQL);
  31104. end;
  31105. function TSQLRest.EngineUpdateFieldIncrement(TableModelIndex: integer;
  31106. ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
  31107. var Value: Int64;
  31108. Table: TSQLRecordClass;
  31109. begin
  31110. if (TableModelIndex<0) or (ID<0) then
  31111. result := false else begin
  31112. Table := Model.Tables[TableModelIndex];
  31113. result := OneFieldValue(Table,FieldName,'ID=?',[],[ID],Value) and
  31114. UpdateField(Table,ID,FieldName,[Value+Increment]);
  31115. end;
  31116. end;
  31117. procedure TSQLRest.SetRoutingClass(aServicesRouting: TSQLRestServerURIContextClass);
  31118. begin
  31119. if self<>nil then
  31120. if aServicesRouting<>fRoutingClass then
  31121. if (aServicesRouting=nil) or (aServicesRouting=TSQLRestServerURIContext) then
  31122. raise EServiceException.CreateUTF8('Unexpected %.SetRoutingClass(%)',
  31123. [self,aServicesRouting]) else
  31124. fRoutingClass := aServicesRouting;
  31125. end;
  31126. function TSQLRest.MultiFieldValue(Table: TSQLRecordClass;
  31127. const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8;
  31128. WhereID: TID): boolean;
  31129. begin
  31130. result := MultiFieldValue(Table,FieldName,FieldValue,
  31131. 'RowID=:('+Int64ToUtf8(WhereID)+'):');
  31132. end;
  31133. function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName,
  31134. WhereClause: RawUTF8): RawUTF8;
  31135. var Res: array[0..0] of RawUTF8;
  31136. begin
  31137. if MultiFieldValue(Table,[FieldName],Res,WhereClause) then
  31138. result := Res[0] else
  31139. result := '';
  31140. end;
  31141. function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8;
  31142. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): RawUTF8;
  31143. begin
  31144. result := OneFieldValue(Table,FieldName,FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere));
  31145. end;
  31146. function TSQLRest.OneFieldValue(Table: TSQLRecordClass;
  31147. const FieldName: RawUTF8; const WhereClauseFmt: RawUTF8;
  31148. const Args, Bounds: array of const): RawUTF8;
  31149. begin
  31150. result := OneFieldValue(Table,FieldName,FormatUTF8(WhereClauseFmt,Args,Bounds));
  31151. end;
  31152. function TSQLRest.OneFieldValue(Table: TSQLRecordClass;
  31153. const FieldName: RawUTF8; WhereID: TID): RawUTF8;
  31154. var Res: array[0..0] of RawUTF8;
  31155. begin
  31156. if (WhereID>0) and
  31157. MultiFieldValue(Table,[FieldName],Res,'RowID=:('+Int64ToUtf8(WhereID)+'):') then
  31158. result := Res[0] else
  31159. result := '';
  31160. end;
  31161. function TSQLRest.MemberExists(Table: TSQLRecordClass; ID: TID): boolean;
  31162. begin
  31163. result := OneFieldValue(Table,'RowID',ID)<>'';
  31164. end;
  31165. function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8;
  31166. const WhereClauseFmt: RawUTF8; const Args, Bounds: array of const;
  31167. out Data: Int64): boolean;
  31168. var Res: array[0..0] of RawUTF8;
  31169. err: integer;
  31170. where: RawUTF8;
  31171. begin
  31172. result := false;
  31173. where := FormatUTF8(WhereClauseFmt,Args,Bounds);
  31174. if MultiFieldValue(Table,[FieldName],Res,where) then
  31175. if Res[0]<>'' then begin
  31176. Data := GetInt64(pointer(Res[0]),err);
  31177. if err=0 then
  31178. result := true;
  31179. end;
  31180. end;
  31181. function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName,
  31182. WhereClause: RawUTF8; out Data: TRawUTF8DynArray): boolean;
  31183. var i: integer;
  31184. T: TSQLTableJSON;
  31185. begin
  31186. result := false;
  31187. T := MultiFieldValues(Table,FieldName,WhereClause);
  31188. if T<>nil then
  31189. try
  31190. if (T.FieldCount<>1) or (T.fRowCount<=0) then
  31191. exit;
  31192. // get row values
  31193. SetLength(Data,T.fRowCount);
  31194. for i := 1 to T.fRowCount do // ignore fResults[0] i.e. field name
  31195. Data[i-1] := T.fResults[i];
  31196. result := true;
  31197. finally
  31198. T.Free;
  31199. end;
  31200. end;
  31201. function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName,
  31202. WhereClause: RawUTF8; Strings: TStrings; IDToIndex: PID=nil): Boolean;
  31203. var Row: integer;
  31204. aID: TID;
  31205. T: TSQLTableJSON;
  31206. begin
  31207. result := false;
  31208. if (Strings<>nil) and (self<>nil) and (Table<>nil) then
  31209. try
  31210. {$ifndef LVCL}
  31211. Strings.BeginUpdate;
  31212. {$endif}
  31213. Strings.Clear;
  31214. T := ExecuteList([Table],
  31215. SQLFromSelect(Table.SQLTableName,'ID,'+FieldName,WhereClause,''));
  31216. if T<>nil then
  31217. try
  31218. if (T.FieldCount=2) and (T.fRowCount>0) then begin
  31219. for Row := 1 to T.fRowCount do begin // ignore Row 0 i.e. field names
  31220. aID := GetInt64(T.Get(Row,0));
  31221. Strings.AddObject(T.GetString(Row,1),pointer(aID));
  31222. if (IDToIndex<>nil) and (aID=IDToIndex^) then begin
  31223. IDToIndex^ := Row-1;
  31224. IDToIndex := nil; // set once
  31225. end;
  31226. end;
  31227. result := true;
  31228. end;
  31229. finally
  31230. T.Free;
  31231. end;
  31232. finally
  31233. {$ifndef LVCL}
  31234. Strings.EndUpdate;
  31235. {$endif}
  31236. end;
  31237. if IDToIndex<>nil then
  31238. IDToIndex^ := -1; // ID not found
  31239. end;
  31240. function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName,
  31241. WhereClause, Separator: RawUTF8): RawUTF8;
  31242. var i, Len, SepLen, L: integer;
  31243. Lens: TIntegerDynArray;
  31244. T: TSQLTableJSON;
  31245. P: PUTF8Char;
  31246. begin
  31247. result := '';
  31248. T := MultiFieldValues(Table,FieldName,WhereClause);
  31249. if T<>nil then
  31250. try
  31251. if (T.FieldCount<>1) or (T.fRowCount<=0) then
  31252. exit;
  31253. // calculate row values CSV needed memory
  31254. SetLength(Lens,T.fRowCount);
  31255. SepLen := length(Separator);
  31256. Len := 0;
  31257. for i := 0 to T.fRowCount-1 do begin // ignore fResults[0] i.e. field name
  31258. Lens[i] := StrLen(T.fResults[i]);
  31259. inc(Len,Lens[i]+SepLen);
  31260. end;
  31261. dec(Len,SepLen);
  31262. SetLength(result,Len);
  31263. // add row values as CSV
  31264. P := pointer(result);
  31265. for i := 1 to T.fRowCount do begin
  31266. L := Lens[i-1];
  31267. if L<>0 then begin
  31268. MoveFast(T.fResults[i]^,P^,L);
  31269. inc(P,L);
  31270. end;
  31271. if i=T.fRowCount then
  31272. break;
  31273. MoveFast(pointer(Separator)^,P^,SepLen);
  31274. inc(P,SepLen);
  31275. end;
  31276. //assert(P-pointer(result)=Len);
  31277. finally
  31278. T.Free;
  31279. end;
  31280. end;
  31281. function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName,
  31282. WhereClause: RawUTF8; var Data: TInt64DynArray; SQL: PRawUTF8=nil): boolean;
  31283. var T: TSQLTableJSON;
  31284. V: Int64;
  31285. Prop: RawUTF8;
  31286. P: PUTF8Char;
  31287. begin
  31288. Data := nil;
  31289. // handle naive expressions like SELECT ID from Table where ID=10
  31290. if IsRowID(pointer(FieldName)) and (length(WhereClause)>2) then begin
  31291. P := pointer(WhereClause);
  31292. GetNextFieldProp(P,Prop);
  31293. if IsRowIDShort(Prop) then
  31294. case P^ of
  31295. '=': begin
  31296. inc(P);
  31297. if PWord(P)^=ord(':')+ord('(')shl 8 then
  31298. inc(P,2); // handle inlined parameters
  31299. SetInt64(P,V);
  31300. if V>0 then begin
  31301. SetLength(Data,1);
  31302. Data[0] := V;
  31303. result := true;
  31304. exit;
  31305. end;
  31306. end;
  31307. 'i','I': if P[1] in ['n','N'] then begin
  31308. P := GotoNextNotSpace(P+2);
  31309. if (P^='(') and (GotoNextNotSpace(P+1)^ in ['0'..'9']) then begin
  31310. CSVToInt64DynArray(P+1,Data);
  31311. if Data<>nil then begin
  31312. result := true;
  31313. exit;
  31314. end;
  31315. end;
  31316. end;
  31317. end;
  31318. end;
  31319. // retrieve the content from database
  31320. result := false;
  31321. T := MultiFieldValues(Table,FieldName,WhereClause);
  31322. if T<>nil then
  31323. try
  31324. if (T.FieldCount<>1) or (T.fRowCount<=0) then
  31325. exit;
  31326. T.GetRowValues(0,Data);
  31327. if SQL<>nil then
  31328. SQL^ := T.QuerySQL;
  31329. result := true;
  31330. finally
  31331. T.Free;
  31332. end;
  31333. end;
  31334. function TSQLRest.SQLComputeForSelect(Table: TSQLRecordClass;
  31335. const FieldNames, WhereClause: RawUTF8): RawUTF8;
  31336. begin
  31337. result := '';
  31338. if (self=nil) or (Table=nil) then
  31339. exit;
  31340. if FieldNames='' then
  31341. result := Model.Props[Table].SQLFromSelectWhere('*',WhereClause) else
  31342. with Table.RecordProps do
  31343. if FieldNames='*' then
  31344. result := SQLFromSelect(SQLTableName,SQLTableRetrieveAllFields,WhereClause,'') else
  31345. if (PosEx(RawUTF8(','),FieldNames,1)=0) and
  31346. (PosEx(RawUTF8('('),FieldNames,1)=0) and
  31347. not IsFieldName(FieldNames) then
  31348. result := '' else // prevent SQL error
  31349. result := SQLFromSelect(SQLTableName,FieldNames,WhereClause,'');
  31350. end;
  31351. function TSQLRest.MultiFieldValues(Table: TSQLRecordClass;
  31352. const FieldNames, WhereClause: RawUTF8): TSQLTableJSON;
  31353. var sql: RawUTF8;
  31354. begin
  31355. sql := SQLComputeForSelect(Table,FieldNames,WhereClause);
  31356. if sql='' then
  31357. result := nil else
  31358. result := ExecuteList([Table],sql);
  31359. end;
  31360. function TSQLRest.MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8;
  31361. const WhereClauseFormat: RawUTF8; const BoundsSQLWhere: array of const): TSQLTableJSON;
  31362. begin
  31363. result := MultiFieldValues(Table,FieldNames,FormatUTF8(
  31364. WhereClauseFormat,[],BoundsSQLWhere));
  31365. end;
  31366. function TSQLRest.MultiFieldValues(Table: TSQLRecordClass;
  31367. const FieldNames: RawUTF8; const WhereClauseFormat: RawUTF8;
  31368. const Args, Bounds: array of const): TSQLTableJSON;
  31369. begin
  31370. result := MultiFieldValues(Table,FieldNames,FormatUTF8(WhereClauseFormat,Args,Bounds));
  31371. end;
  31372. function TSQLRest.MultiFieldValue(Table: TSQLRecordClass;
  31373. const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8;
  31374. const WhereClause: RawUTF8): boolean;
  31375. var SQL: RawUTF8;
  31376. n,i: integer;
  31377. T: TSQLTableJSON;
  31378. begin
  31379. result := false;
  31380. n := length(FieldName);
  31381. if (self<>nil) and (Table<>nil) and (n=length(FieldValue)) then
  31382. with Table.RecordProps do begin
  31383. if (n=1) and IdemPChar(pointer(FieldName[0]),'COUNT(*)') then
  31384. SQL := 'SELECT COUNT(*) FROM '+SQLTableName+SQLFromWhere(WhereClause) else begin
  31385. for i := 0 to high(FieldName) do
  31386. if not IsFieldNameOrFunction(FieldName[i]) then
  31387. exit else // prevent SQL error or security breach
  31388. if SQL='' then
  31389. SQL := 'SELECT '+FieldName[i] else
  31390. SQL := SQL+','+FieldName[i];
  31391. SQL := SQL+' FROM '+SQLTableName+SQLFromWhere(WhereClause)+' LIMIT 1';
  31392. end;
  31393. T := ExecuteList([Table],SQL);
  31394. if T<>nil then
  31395. try
  31396. if (T.FieldCount<>length(FieldName)) or (T.fRowCount<=0) then
  31397. exit;
  31398. // get field values from the first (and unique) row
  31399. for i := 0 to T.FieldCount-1 do
  31400. FieldValue[i] := T.fResults[T.FieldCount+i];
  31401. result := true;
  31402. finally
  31403. T.Free;
  31404. end;
  31405. end;
  31406. end;
  31407. function TSQLRest.Retrieve(const SQLWhere: RawUTF8; Value: TSQLRecord;
  31408. const aCustomFieldsCSV: RawUTF8): boolean;
  31409. var T: TSQLTable;
  31410. begin
  31411. result := false;
  31412. if (self=nil) or (Value=nil) then
  31413. exit;
  31414. T := MultiFieldValues(PSQLRecordClass(Value)^,aCustomFieldsCSV,SQLWhere);
  31415. if T<>nil then
  31416. try
  31417. if T.fRowCount>=1 then begin
  31418. Value.FillFrom(T,1); // fetch data from first result row
  31419. result := true;
  31420. end else
  31421. Value.fID := 0;
  31422. finally
  31423. T.Free;
  31424. end;
  31425. end;
  31426. function TSQLRest.RetrieveList(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8;
  31427. const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): TObjectList;
  31428. var T: TSQLTable;
  31429. begin
  31430. result := nil;
  31431. if (self=nil) or (Table=nil) then
  31432. exit;
  31433. T := MultiFieldValues(Table,aCustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere);
  31434. if T<>nil then
  31435. try
  31436. result := TObjectList.Create;
  31437. T.ToObjectList(result,Table);
  31438. finally
  31439. T.Free;
  31440. end;
  31441. end;
  31442. function TSQLRest.RetrieveListJSON(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8;
  31443. const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8;
  31444. aForceAJAX: boolean): RawJSON;
  31445. begin
  31446. result := RetrieveListJSON(Table,
  31447. FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere),aCustomFieldsCSV,aForceAJAX)
  31448. end;
  31449. function TSQLRest.RetrieveListJSON(Table: TSQLRecordClass; const SQLWhere: RawUTF8;
  31450. const aCustomFieldsCSV: RawUTF8; aForceAJAX: boolean): RawJSON;
  31451. var sql: RawUTF8;
  31452. begin
  31453. sql := SQLComputeForSelect(Table,aCustomFieldsCSV,SQLWhere);
  31454. if sql='' then
  31455. result := '' else
  31456. result := EngineList(sql,aForceAJAX);
  31457. end;
  31458. function TSQLRest.RetrieveListObjArray(var ObjArray; Table: TSQLRecordClass;
  31459. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  31460. const aCustomFieldsCSV: RawUTF8): boolean;
  31461. var T: TSQLTable;
  31462. begin
  31463. result := false;
  31464. if (self=nil) or (Table=nil) then
  31465. exit;
  31466. T := MultiFieldValues(Table,aCustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere);
  31467. if T<>nil then
  31468. try
  31469. result := T.ToObjArray(ObjArray,Table);
  31470. finally
  31471. T.Free;
  31472. end;
  31473. end;
  31474. procedure TSQLRest.AppendListAsJsonArray(Table: TSQLRecordClass;
  31475. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  31476. const OutputFieldName: RawUTF8; W: TJSONSerializer; const CustomFieldsCSV: RawUTF8);
  31477. var Rec: TSQLRecord;
  31478. begin
  31479. if (self=nil) or (Table=nil) or (W=nil) then
  31480. exit;
  31481. Rec := Table.CreateAndFillPrepare(Self,FormatSQLWhere,BoundsSQLWhere,CustomFieldsCSV);
  31482. try
  31483. Rec.AppendFillAsJsonArray(OutputFieldName,W,Rec.fFill.TableMapFields);
  31484. finally
  31485. Rec.Free;
  31486. end;
  31487. end;
  31488. {$ifndef NOVARIANTS}
  31489. function TSQLRest.RetrieveDocVariantArray(Table: TSQLRecordClass;
  31490. const ObjectName: RawUTF8;
  31491. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  31492. const CustomFieldsCSV: RawUTF8; FirstRecordID,LastRecordID: PID): variant;
  31493. var T: TSQLTable;
  31494. res: variant;
  31495. begin
  31496. TVarData(res).VType := varNull;
  31497. if (self<>nil) and (Table<>nil) then begin
  31498. T := MultiFieldValues(Table,CustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere);
  31499. if T<>nil then
  31500. try
  31501. T.ToDocVariant(res,false); // readonly=false -> TDocVariant dvArray
  31502. if FirstRecordID<>nil then
  31503. FirstRecordID^ := T.IDColumnHiddenValue(1);
  31504. if LastRecordID<>nil then
  31505. LastRecordID^ := T.IDColumnHiddenValue(T.fRowCount);
  31506. finally
  31507. T.Free;
  31508. end;
  31509. end;
  31510. if ObjectName<>'' then
  31511. result := _ObjFast([ObjectName,res]) else
  31512. result := res;
  31513. end;
  31514. function TSQLRest.RetrieveOneFieldDocVariantArray(Table: TSQLRecordClass;
  31515. const FieldName, FormatSQLWhere: RawUTF8;
  31516. const BoundsSQLWhere: array of const): variant;
  31517. var T: TSQLTable;
  31518. row: Integer;
  31519. res: TDocVariantData absolute result;
  31520. begin
  31521. VarClear(result);
  31522. if (self<>nil) and (Table<>nil) then begin
  31523. T := MultiFieldValues(Table,FieldName,FormatSQLWhere,BoundsSQLWhere);
  31524. if T<>nil then
  31525. try
  31526. res.InitFast(T.RowCount,dvArray);
  31527. res.SetCount(T.RowCount);
  31528. for row := 1 to T.RowCount do
  31529. T.GetAsVariant(row,0,res.Values[row-1],false,false,false,JSON_OPTIONS_FAST);
  31530. finally
  31531. T.Free;
  31532. end;
  31533. end;
  31534. end;
  31535. function TSQLRest.RetrieveDocVariantArray(Table: TSQLRecordClass;
  31536. const ObjectName, CustomFieldsCSV: RawUTF8; FirstRecordID,LastRecordID: PID): variant;
  31537. begin
  31538. result := RetrieveDocVariantArray(Table,ObjectName,'',[],CustomFieldsCSV,
  31539. FirstRecordID,LastRecordID);
  31540. end;
  31541. function TSQLRest.RetrieveDocVariant(Table: TSQLRecordClass;
  31542. const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const;
  31543. const CustomFieldsCSV: RawUTF8): variant;
  31544. var T: TSQLTable;
  31545. bits: TSQLFieldBits;
  31546. Rec: TSQLRecord;
  31547. ID: TID;
  31548. begin
  31549. SetVariantNull(result);
  31550. if (self<>nil) and (Table<>nil) then begin
  31551. with Table.RecordProps do // optimized primary key direct access
  31552. if Cache.IsCached(Table) and (length(BoundsSQLWhere)=1) and
  31553. VarRecToInt64(BoundsSQLWhere[0],Int64(ID)) and
  31554. FieldBitsFromCSV(CustomFieldsCSV,bits) then
  31555. if IsZero(bits) then
  31556. exit else
  31557. if bits-SimpleFieldsBits[soSelect]=[] then
  31558. if IdemPropNameU('RowID=?',FormatSQLWhere) or
  31559. IdemPropNameU('ID=?',FormatSQLWhere) then begin
  31560. Rec := Table.Create(self,ID);
  31561. try
  31562. Rec.GetAsDocVariant(True,bits,result);
  31563. finally
  31564. Rec.Free;
  31565. end;
  31566. exit;
  31567. end;
  31568. T := MultiFieldValues(Table,CustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere);
  31569. if T<>nil then
  31570. try
  31571. T.ToDocVariant(1,result)
  31572. finally
  31573. T.Free;
  31574. end;
  31575. end;
  31576. end;
  31577. {$endif}
  31578. function TSQLRest.Retrieve(aID: TID; Value: TSQLRecord;
  31579. ForUpdate: boolean): boolean;
  31580. var TableIndex: integer; // used by EngineRetrieve() for SQL statement caching
  31581. Resp: RawUTF8;
  31582. begin // this version handles locking and use fast EngineRetrieve() method
  31583. // check parameters
  31584. result := false;
  31585. if Value=nil then
  31586. exit; // avoid GPF
  31587. Value.fID := 0;
  31588. if (self=nil) or (aID=0) then
  31589. exit;
  31590. TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^);
  31591. // try to lock before retrieval (if ForUpdate)
  31592. if ForUpdate and not Model.Lock(TableIndex,aID) then
  31593. exit;
  31594. // try to retrieve existing JSON from internal cache
  31595. Resp := fCache.Retrieve(TableIndex,aID);
  31596. if Resp='' then begin
  31597. // get JSON object '{...}' in Resp from corresponding EngineRetrieve() method
  31598. Resp := EngineRetrieve(TableIndex,aID);
  31599. if Resp='' then begin
  31600. fCache.NotifyDeletion(TableIndex,aID);
  31601. exit;
  31602. end;
  31603. fCache.Notify(Tableindex,aID,Resp,soSelect);
  31604. end;
  31605. Value.fID := aID; // Resp may not contain the "RowID": field after Update
  31606. // fill Value from JSON if was correctly retrieved
  31607. Value.FillFrom(Resp);
  31608. result := true;
  31609. end;
  31610. function TSQLRest.Retrieve(const WhereClauseFmt: RawUTF8;
  31611. const Args,Bounds: array of const; Value: TSQLRecord; const aCustomFieldsCSV: RawUTF8): boolean;
  31612. var where: RawUTF8;
  31613. begin
  31614. where := FormatUTF8(WhereClauseFmt,Args,Bounds);
  31615. result := Retrieve(where,Value,aCustomFieldsCSV);
  31616. end;
  31617. function TSQLRest.Retrieve(Reference: TRecordReference; ForUpdate: boolean): TSQLRecord;
  31618. var aClass: TSQLRecordClass;
  31619. begin
  31620. result := nil;
  31621. if (self=nil) or (RecordRef(Reference).ID=0) then
  31622. exit;
  31623. aClass := RecordRef(Reference).Table(Model);
  31624. if aClass=nil then
  31625. exit;
  31626. result := aClass.Create(self,RecordRef(Reference).ID,ForUpdate);
  31627. if result.fID=0 then
  31628. FreeAndNil(result); // error during value retrieval
  31629. end;
  31630. function TSQLRest.Retrieve(aPublishedRecord, aValue: TSQLRecord): boolean;
  31631. begin
  31632. result := Retrieve(aPublishedRecord.ID,aValue);
  31633. end;
  31634. function TSQLRest.UnLock(Rec: TSQLRecord): boolean;
  31635. begin
  31636. if (self=nil) or (Rec=nil) or (Rec.fID<=0) then
  31637. result := false else
  31638. result := UnLock(PSQLRecordClass(Rec)^,Rec.fID);
  31639. end;
  31640. procedure TSQLRest.Commit(SessionID: cardinal; RaiseException: boolean);
  31641. begin
  31642. if self<>nil then begin
  31643. fAcquireExecution[execORMWrite].Safe.Lock;
  31644. try
  31645. if (fTransactionActiveSession<>0) and
  31646. (fTransactionActiveSession=SessionID) then begin
  31647. fTransactionActiveSession := 0; // by default, just release flag
  31648. fTransactionTable := nil;
  31649. end;
  31650. finally
  31651. fAcquireExecution[execORMWrite].Safe.UnLock;
  31652. end;
  31653. end;
  31654. end;
  31655. procedure TSQLRest.RollBack(SessionID: cardinal);
  31656. begin
  31657. if self<>nil then begin
  31658. fAcquireExecution[execORMWrite].Safe.Lock;
  31659. try
  31660. if (fTransactionActiveSession<>0) and
  31661. (fTransactionActiveSession=SessionID) then begin
  31662. fTransactionActiveSession := 0; // by default, just release flag
  31663. fTransactionTable := nil;
  31664. end;
  31665. finally
  31666. fAcquireExecution[execORMWrite].Safe.UnLock;
  31667. end;
  31668. end;
  31669. end;
  31670. function TSQLRest.TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal): boolean;
  31671. begin
  31672. result := false;
  31673. fAcquireExecution[execORMWrite].Safe.Lock;
  31674. try
  31675. if fTransactionActiveSession=0 then begin // nested transactions are not allowed
  31676. fTransactionActiveSession := SessionID;
  31677. fTransactionTable := aTable;
  31678. result := true;
  31679. end;
  31680. finally
  31681. fAcquireExecution[execORMWrite].Safe.UnLock;
  31682. end;
  31683. end;
  31684. function TSQLRest.TransactionActiveSession: cardinal;
  31685. begin
  31686. if self=nil then
  31687. result := 0 else begin
  31688. fAcquireExecution[execORMWrite].Safe.Lock;
  31689. try
  31690. result := fTransactionActiveSession;
  31691. finally
  31692. fAcquireExecution[execORMWrite].Safe.UnLock;
  31693. end;
  31694. end;
  31695. end;
  31696. function TSQLRest.BatchSend(Batch: TSQLRestBatch;
  31697. var Results: TIDDynArray): integer;
  31698. var Data: RawUTF8;
  31699. begin
  31700. result := HTML_BADREQUEST;
  31701. if (self=nil) or (Batch=nil) then // no opened BATCH sequence
  31702. exit;
  31703. if Batch.PrepareForSending(Data) then
  31704. if Data='' then // i.e. Batch.Count=0
  31705. result := HTML_SUCCESS else
  31706. try
  31707. result := EngineBatchSend(Batch.Table,Data,Results,Batch.Count);
  31708. except
  31709. on Exception do // e.g. from TSQLRestServer.EngineBatchSend()
  31710. result := HTML_SERVERERROR;
  31711. end;
  31712. end;
  31713. function TSQLRest.BatchSend(Batch: TSQLRestBatch): integer;
  31714. var Res: TIDDynArray;
  31715. begin
  31716. result := BatchSend(Batch,Res);
  31717. end;
  31718. function TSQLRest.RecordCanBeUpdated(Table: TSQLRecordClass; ID: TID;
  31719. Action: TSQLEvent; ErrorMsg: PRawUTF8=nil): boolean;
  31720. begin
  31721. result := true; // accept by default -> override this method to customize this
  31722. end;
  31723. function TSQLRest.Delete(Table: TSQLRecordClass; ID: TID): boolean;
  31724. var TableIndex: integer;
  31725. begin
  31726. TableIndex := Model.GetTableIndexExisting(Table);
  31727. if not RecordCanBeUpdated(Table,ID,seDelete) then
  31728. result := false else begin
  31729. fCache.NotifyDeletion(TableIndex,ID);
  31730. result := EngineDelete(TableIndex,ID);
  31731. end;
  31732. end;
  31733. function TSQLRest.InternalDeleteNotifyAndGetIDs(Table: TSQLRecordClass;
  31734. const SQLWhere: RawUTF8; var IDs: TIDDynArray): boolean;
  31735. var i: integer;
  31736. begin
  31737. result := false;
  31738. if OneFieldValues(Table,'RowID',SQLWhere,TInt64DynArray(IDs)) and
  31739. (IDs<>nil) then begin
  31740. for i := 0 to high(IDs) do
  31741. if not RecordCanBeUpdated(Table,IDs[i],seDelete) then
  31742. exit;
  31743. for i := 0 to high(IDs) do
  31744. fCache.NotifyDeletion(Table,IDs[i]);
  31745. end;
  31746. result := true;
  31747. end;
  31748. function TSQLRest.Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean;
  31749. var IDs: TIDDynArray;
  31750. begin
  31751. if InternalDeleteNotifyAndGetIDs(Table,SQLWhere,IDs) then
  31752. result := EngineDeleteWhere(Model.GetTableIndexExisting(Table),SQLWhere,IDs) else
  31753. result := false;
  31754. end;
  31755. function TSQLRest.Delete(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8;
  31756. const BoundsSQLWhere: array of const): boolean;
  31757. begin
  31758. result := Delete(Table,FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere));
  31759. end;
  31760. function TSQLRest.Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits;
  31761. DoNotAutoComputeFields: boolean): boolean;
  31762. var JSONValues: RawUTF8;
  31763. TableIndex: integer;
  31764. FieldBits: TSQLFieldBits;
  31765. begin
  31766. if (self=nil) or (Value=nil) or (Value.fID=0) or
  31767. not RecordCanBeUpdated(PSQLRecordClass(Value)^,Value.fID,seUpdate) then begin
  31768. result := false; // current user don't have enough right to update this record
  31769. exit;
  31770. end;
  31771. TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^);
  31772. if not DoNotAutoComputeFields then
  31773. Value.ComputeFieldsBeforeWrite(self,seUpdate); // update sftModTime fields
  31774. if IsZero(CustomFields) then
  31775. if (Value.fFill<>nil) and (Value.fFill.Table<>nil) and
  31776. (Value.fFill.fTableMapRecordManyInstances=nil) then
  31777. // within FillPrepare/FillOne loop: update ID, TModTime and mapped fields
  31778. FieldBits := Value.fFill.fTableMapFields+Value.RecordProps.FieldBits[sftModTime] else
  31779. // update all simple/custom fields (also for FillPrepareMany)
  31780. FieldBits := Value.RecordProps.SimpleFieldsBits[soUpdate] else
  31781. // CustomFields<>[] -> update specified (and TModTime fields)
  31782. if DoNotAutoComputeFields then
  31783. FieldBits := CustomFields else
  31784. FieldBits := CustomFields+Value.RecordProps.FieldBits[sftModTime];
  31785. if IsZero(FieldBits) then begin
  31786. result := true; // a TSQLRecord with NO simple fields (e.g. ID/blob pair)
  31787. exit;
  31788. end;
  31789. fCache.Notify(Value,soUpdate); // will serialize Value (JSONValues may not be enough)
  31790. JSONValues := Value.GetJSONValues(true,false,FieldBits);
  31791. result := EngineUpdate(TableIndex,Value.fID,JSONValues);
  31792. end;
  31793. function TSQLRest.Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8;
  31794. DoNotAutoComputeFields: boolean): boolean;
  31795. begin
  31796. if (self=nil) or (Value=nil) then
  31797. result := false else
  31798. result := Update(Value,Value.RecordProps.FieldBitsFromCSV(CustomCSVFields),
  31799. DoNotAutoComputeFields);
  31800. end;
  31801. function TSQLRest.Update(aTable: TSQLRecordClass; aID: TID;
  31802. const aSimpleFields: array of const): boolean;
  31803. var Value: TSQLRecord;
  31804. begin
  31805. result := false; // means error
  31806. if (self=nil) or (aTable=nil) or (aID=0) then
  31807. exit;
  31808. Value := aTable.Create;
  31809. try
  31810. if not Value.SimplePropertiesFill(aSimpleFields) then
  31811. exit;
  31812. Value.fID := aID;
  31813. result := Update(Value);
  31814. finally
  31815. Value.Free;
  31816. end;
  31817. end;
  31818. function TSQLRest.UpdateField(Table: TSQLRecordClass; ID: TID;
  31819. const FieldName: RawUTF8; const FieldValue: array of const): boolean;
  31820. begin
  31821. result := UpdateField(Table,'RowID',[ID],FieldName,FieldValue);
  31822. end;
  31823. function TSQLRest.UpdateField(Table: TSQLRecordClass;
  31824. const WhereFieldName: RawUTF8; const WhereFieldValue: array of const;
  31825. const FieldName: RawUTF8; const FieldValue: array of const): boolean;
  31826. var TableIndex: integer;
  31827. SetValue,WhereValue: RawUTF8;
  31828. begin
  31829. result := false;
  31830. if (length(FieldValue)<>1) or (WhereFieldName='') or (length(WhereFieldValue)<>1) then
  31831. exit;
  31832. VarRecToInlineValue(WhereFieldValue[0],WhereValue);
  31833. VarRecToInlineValue(FieldValue[0],SetValue);
  31834. TableIndex := Model.GetTableIndexExisting(Table);
  31835. result := EngineUpdateField(TableIndex,FieldName,SetValue,WhereFieldName,WhereValue);
  31836. end;
  31837. {$ifndef NOVARIANTS}
  31838. function TSQLRest.UpdateField(Table: TSQLRecordClass; ID: TID;
  31839. const FieldName: RawUTF8; const FieldValue: Variant): boolean;
  31840. begin
  31841. result := UpdateField(Table,'RowID',ID,FieldName,FieldValue);
  31842. end;
  31843. function TSQLRest.UpdateField(Table: TSQLRecordClass;
  31844. const WhereFieldName: RawUTF8; const WhereFieldValue: Variant;
  31845. const FieldName: RawUTF8; const FieldValue: Variant): boolean;
  31846. var TableIndex: integer;
  31847. SetValue,WhereValue: RawUTF8;
  31848. begin
  31849. VariantToInlineValue(WhereFieldValue,WhereValue);
  31850. VariantToInlineValue(FieldValue,SetValue);
  31851. TableIndex := Model.GetTableIndexExisting(Table);
  31852. result := EngineUpdateField(TableIndex,FieldName,SetValue,WhereFieldName,WhereValue);
  31853. end;
  31854. function TSQLRest.UpdateField(Table: TSQLRecordClass;
  31855. const IDs: array of Int64; const FieldName: RawUTF8; const FieldValue: variant): boolean;
  31856. var csv: RawUTF8;
  31857. SetValue: RawUTF8;
  31858. begin
  31859. VariantToInlineValue(FieldValue,SetValue);
  31860. csv := Int64DynArrayToCSV(IDs,length(IDs));
  31861. result := ExecuteFmt('update % set %=:(%): where rowid in (%)',
  31862. [Table.SQLTableName,FieldName,SetValue,csv]);
  31863. end;
  31864. {$endif NOVARIANTS}
  31865. function TSQLRest.UpdateFieldIncrement(Table: TSQLRecordClass; ID: TID;
  31866. const FieldName: RawUTF8; Increment: Int64): boolean;
  31867. var tableIndex: integer;
  31868. begin
  31869. if ID<>0 then begin
  31870. tableIndex := Model.GetTableIndexExisting(Table);
  31871. result := EngineUpdateFieldIncrement(tableIndex,ID,FieldName,Increment);
  31872. if fCache<>nil then
  31873. fCache.NotifyDeletion(tableIndex,ID);
  31874. end else
  31875. result := false;
  31876. end;
  31877. procedure TSQLRest.GetJSONValuesForAdd(TableIndex: integer; Value: TSQLRecord;
  31878. ForceID, DoNotAutoComputeFields, WithBlobs: boolean;
  31879. CustomFields: PSQLFieldBits; var result: RawUTF8);
  31880. var fields: TSQLFieldBits;
  31881. begin
  31882. if not DoNotAutoComputeFields then // update TModTime/TCreateTime fields
  31883. Value.ComputeFieldsBeforeWrite(self,seAdd);
  31884. if Model.TableProps[TableIndex].Kind in INSERT_WITH_ID then
  31885. ForceID := true;
  31886. if (Model.fIDGenerator<>nil) and (Model.fIDGenerator[TableIndex]<>nil) then begin
  31887. Value.fID := Model.fIDGenerator[TableIndex].ComputeNew;
  31888. ForceID := true;
  31889. end else
  31890. if Value.fID=0 then
  31891. ForceID := false;
  31892. if CustomFields <> nil then
  31893. if DoNotAutoComputeFields then
  31894. fields := CustomFields^ else
  31895. fields := CustomFields^+Value.RecordProps.ComputeBeforeAddFieldsBits else
  31896. if withBlobs then
  31897. fields := Value.RecordProps.CopiableFieldsBits else
  31898. fields := Value.RecordProps.SimpleFieldsBits[soInsert];
  31899. if (not ForceID) and IsZero(fields) then
  31900. result := '' else
  31901. result := Value.GetJSONValues(true,ForceID,fields);
  31902. end;
  31903. function TSQLRest.InternalAdd(Value: TSQLRecord; SendData: boolean;
  31904. CustomFields: PSQLFieldBits; ForceID, DoNotAutoComputeFields: boolean): TID;
  31905. var json: RawUTF8;
  31906. TableIndex: integer;
  31907. begin
  31908. if Value=nil then begin
  31909. result := 0;
  31910. exit;
  31911. end;
  31912. TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^);
  31913. if SendData then
  31914. GetJSONValuesForAdd(TableIndex,Value,ForceID,DoNotAutoComputeFields,false,CustomFields,json) else
  31915. json := '';
  31916. // on success, returns the new ROWID value; on error, returns 0
  31917. result := EngineAdd(TableIndex,json); // will call static if necessary
  31918. // on success, Value.ID is updated with the new ROWID
  31919. Value.fID := result;
  31920. if SendData and (result<>0) then
  31921. fCache.Notify(PSQLRecordClass(Value)^,result,json,soInsert);
  31922. end;
  31923. function TSQLRest.Add(Value: TSQLRecord; SendData,ForceID,DoNotAutoComputeFields: boolean): TID;
  31924. begin
  31925. result := InternalAdd(Value,SendData,nil,ForceID,DoNotAutoComputeFields);
  31926. end;
  31927. function TSQLRest.Add(Value: TSQLRecord; const CustomCSVFields: RawUTF8;
  31928. ForceID, DoNotAutoComputeFields: boolean): TID;
  31929. var f: TSQLFieldBits;
  31930. begin
  31931. with Value.RecordProps do
  31932. if CustomCSVFields='*' then // FieldBitsFromCSV('*') would use [soSelect]
  31933. f := SimpleFieldsBits[soInsert] else
  31934. f := FieldBitsFromCSV(CustomCSVFields);
  31935. result := InternalAdd(Value,true,@f,ForceID,DoNotAutoComputeFields);
  31936. end;
  31937. function TSQLRest.Add(Value: TSQLRecord; const CustomFields: TSQLFieldBits;
  31938. ForceID, DoNotAutoComputeFields: boolean): TID;
  31939. begin
  31940. result := InternalAdd(Value,true,@CustomFields,ForceID,DoNotAutoComputeFields);
  31941. end;
  31942. function TSQLRest.Add(aTable: TSQLRecordClass; const aSimpleFields: array of const;
  31943. ForcedID: TID=0): TID;
  31944. var Value: TSQLRecord;
  31945. begin
  31946. result := 0; // means error
  31947. if (self=nil) or (aTable=nil) then
  31948. exit;
  31949. Value := aTable.Create;
  31950. try
  31951. if Value.SimplePropertiesFill(aSimpleFields) then begin
  31952. if ForcedID<>0 then
  31953. Value.fID := ForcedID;
  31954. result := Add(Value,true,(ForcedID<>0));
  31955. end;
  31956. finally
  31957. Value.Free;
  31958. end;
  31959. end;
  31960. function TSQLRest.AddWithBlobs(Value: TSQLRecord;
  31961. ForceID, DoNotAutoComputeFields: boolean): TID;
  31962. var TableIndex: integer;
  31963. json: RawUTF8;
  31964. begin
  31965. if Value=nil then begin
  31966. result := 0;
  31967. exit;
  31968. end;
  31969. TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^);
  31970. GetJSONValuesForAdd(TableIndex,Value,ForceID,DoNotAutoComputeFields,true,nil,json);
  31971. // on success, returns the new ROWID value; on error, returns 0
  31972. result := EngineAdd(TableIndex,json); // will call static if necessary
  31973. // on success, Value.ID is updated with the new ROWID
  31974. Value.fID := result;
  31975. // here fCache.Notify is not called, since the JSONValues is verbose
  31976. end;
  31977. function TSQLRest.AddOrUpdate(Value: TSQLRecord; ForceID: boolean): TID;
  31978. begin
  31979. if (self=nil) or (Value=nil) then begin
  31980. result := 0;
  31981. exit;
  31982. end;
  31983. if ForceID or (Value.fID=0) then begin
  31984. result := Add(Value,true,ForceID);
  31985. if (result<>0) or (Value.fID=0) then
  31986. exit;
  31987. end;
  31988. if Update(Value) then
  31989. result := Value.fID else
  31990. result := 0;
  31991. end;
  31992. procedure TSQLRest.QueryAddCustom(aTypeInfo: pointer; aEvent: TSQLQueryEvent;
  31993. const aOperators: TSQLQueryOperators);
  31994. var Enum: PEnumType;
  31995. i,n: integer;
  31996. begin
  31997. if (self=nil) or not Assigned(aEvent) or
  31998. (aTypeInfo=nil) or (PTypeInfo(aTypeInfo)^.Kind<>tkEnumeration) then
  31999. exit;
  32000. Enum := PTypeInfo(aTypeInfo)^.EnumBaseType;
  32001. n := length(QueryCustom);
  32002. SetLength(QueryCustom,n+Enum^.MaxValue+1);
  32003. for i := 0 to Enum^.MaxValue do
  32004. with QueryCustom[i+n] do begin
  32005. EnumType := Enum;
  32006. EnumIndex := i;
  32007. Event := aEvent;
  32008. Operators := aOperators;
  32009. end;
  32010. end;
  32011. class function TSQLRest.QueryIsTrue(aTable: TSQLRecordClass; aID: TID;
  32012. FieldType: TSQLFieldType; Value: PUTF8Char; Operator: integer;
  32013. Reference: PUTF8Char): boolean;
  32014. begin // use mostly the same fast comparison functions as for sorting
  32015. result := false;
  32016. if aID=0 then
  32017. exit; // invalid input field
  32018. if Reference=nil then
  32019. exit; // avoid most GPF
  32020. if FieldType=sftMany then
  32021. exit; // nothing is stored directly, but in a separate pivot table
  32022. if FieldType in [sftUnknown,sftBlob,sftBlobDynArray,sftBlobCustom,sftObject,
  32023. sftUTF8Custom{$ifndef NOVARIANTS},sftVariant,sftNullable{$endif}] then
  32024. FieldType := sftUTF8Text; // unknown or blob fields are compared as UTF-8
  32025. { TODO: handle proper sftBlobDynArray/sftBlobCustom/sftBlobRecord comparison }
  32026. case TSQLQueryOperator(Operator) of
  32027. qoNone:
  32028. result := true;
  32029. qoEqualTo:
  32030. result := SQLFieldTypeComp[FieldType](Value,Reference)=0;
  32031. qoNotEqualTo:
  32032. result := SQLFieldTypeComp[FieldType](Value,Reference)<>0;
  32033. qoLessThan:
  32034. result := SQLFieldTypeComp[FieldType](Value,Reference)<0;
  32035. qoLessThanOrEqualTo:
  32036. result := SQLFieldTypeComp[FieldType](Value,Reference)<=0;
  32037. qoGreaterThan:
  32038. result := SQLFieldTypeComp[FieldType](Value,Reference)>0;
  32039. qoGreaterThanOrEqualTo:
  32040. result := SQLFieldTypeComp[FieldType](Value,Reference)>=0;
  32041. qoEqualToWithCase:
  32042. result := StrComp(Value,Reference)=0;
  32043. qoNotEqualToWithCase:
  32044. result := StrComp(Value,Reference)<>0;
  32045. qoContains:
  32046. result := PosIU(Reference,Value)<>0;
  32047. qoBeginWith:
  32048. result := IdemPCharU(Value,Reference);
  32049. qoSoundsLikeEnglish,
  32050. qoSoundsLikeFrench,
  32051. qoSoundsLikeSpanish:
  32052. result := PSynSoundEx(Reference)^.UTF8(Value);
  32053. end;
  32054. end;
  32055. function TSQLRest.RetrieveBlob(Table: TSQLRecordClass; aID: TID;
  32056. const BlobFieldName: RawUTF8; out BlobStream: THeapMemoryStream): boolean;
  32057. var BlobData: TSQLRawBlob;
  32058. begin
  32059. BlobStream := THeapMemoryStream.Create;
  32060. result := RetrieveBlob(Table,aID,BlobFieldName,BlobData);
  32061. if not result or (BlobData='') then
  32062. exit;
  32063. BlobStream.Write(pointer(BlobData)^,length(BlobData));
  32064. BlobStream.Seek(0,soFromBeginning); // rewind
  32065. end;
  32066. function TSQLRest.UpdateBlob(Table: TSQLRecordClass; aID: TID;
  32067. const BlobFieldName: RawUTF8; BlobData: TStream): boolean;
  32068. var Blob: TSQLRawBlob;
  32069. L: integer;
  32070. begin
  32071. result := false;
  32072. if (self=nil) or (BlobData=nil) then
  32073. exit;
  32074. L := BlobData.Seek(0,soFromEnd);
  32075. SetLength(Blob,L);
  32076. BlobData.Seek(0,soFromBeginning);
  32077. if BlobData.Read(pointer(Blob)^,L)<>L then
  32078. exit;
  32079. result := UpdateBlob(Table,aID,BlobFieldName,Blob);
  32080. end;
  32081. function TSQLRest.UpdateBlob(Table: TSQLRecordClass; aID: TID;
  32082. const BlobFieldName: RawUTF8; BlobData: pointer; BlobSize: integer): boolean;
  32083. var Blob: TSQLRawBlob;
  32084. begin
  32085. if (self=nil) or (BlobData=nil) or (BlobSize<0) then
  32086. result := false else begin
  32087. SetString(Blob,PAnsiChar(BlobData),BlobSize);
  32088. result := UpdateBlob(Table,aID,BlobFieldName,Blob);
  32089. end;
  32090. end;
  32091. function TSQLRest.RetrieveBlob(Table: TSQLRecordClass; aID: TID;
  32092. const BlobFieldName: RawUTF8; out BlobData: TSQLRawBlob): boolean;
  32093. var BlobField: PPropInfo;
  32094. begin
  32095. result := false;
  32096. if (self=nil) or (aID<=0) then
  32097. exit;
  32098. BlobField := Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName);
  32099. if BlobField=nil then
  32100. exit;
  32101. result := EngineRetrieveBlob(
  32102. Model.GetTableIndexExisting(Table),aID,BlobField,BlobData);
  32103. end;
  32104. function TSQLRest.UpdateBlob(Table: TSQLRecordClass; aID: TID;
  32105. const BlobFieldName: RawUTF8; const BlobData: TSQLRawBlob): boolean;
  32106. var BlobField: PPropInfo;
  32107. begin
  32108. result := false;
  32109. if (self=nil) or (aID<=0) or not RecordCanBeUpdated(Table,aID,seUpdate) then
  32110. exit;
  32111. BlobField := Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName);
  32112. if BlobField=nil then
  32113. exit;
  32114. result := EngineUpdateBlob(
  32115. Model.GetTableIndexExisting(Table),aID,BlobField,BlobData);
  32116. end;
  32117. function TSQLRest.UpdateBlobFields(Value: TSQLRecord): boolean;
  32118. var BlobData: RawByteString;
  32119. TableIndex, i: integer;
  32120. begin
  32121. result := false;
  32122. if (Value=nil) or (Value.fID<=0) then
  32123. exit;
  32124. with Value.RecordProps do
  32125. if BlobFields<>nil then begin
  32126. TableIndex := self.fModel.GetTableIndexExisting(PSQLRecordClass(Value)^);
  32127. for i := 0 to high(BlobFields) do begin
  32128. BlobFields[i].PropInfo.GetLongStrProp(Value,BlobData);
  32129. if not EngineUpdateBlob(TableIndex,Value.fID,BlobFields[i].PropInfo,BlobData) then
  32130. exit;
  32131. end;
  32132. end;
  32133. result := true;
  32134. end;
  32135. function TSQLRest.RetrieveBlobFields(Value: TSQLRecord): boolean;
  32136. var BlobData: TSQLRawBlob;
  32137. TableIndex, i: integer;
  32138. begin
  32139. result := false;
  32140. if (Self=nil) or (Value=nil) or (Value.fID<=0) then
  32141. exit;
  32142. with Value.RecordProps do
  32143. if BlobFields<>nil then begin
  32144. TableIndex := self.fModel.GetTableIndexExisting(PSQLRecordClass(Value)^);
  32145. for i := 0 to high(BlobFields) do
  32146. if EngineRetrieveBlob(TableIndex,Value.fID,BlobFields[i].PropInfo,BlobData) then
  32147. BlobFields[i].PropInfo.SetLongStrProp(Value,BlobData) else
  32148. exit;
  32149. end;
  32150. result := true;
  32151. end;
  32152. function TSQLRest.TableRowCount(Table: TSQLRecordClass): Int64;
  32153. var T: TSQLTableJSON;
  32154. begin
  32155. if (self=nil) or (Table=nil) then
  32156. T := nil else
  32157. T := ExecuteList([Table],'SELECT Count(*) FROM '+Table.RecordProps.SQLTableName);
  32158. if T<>nil then
  32159. try
  32160. Result := T.GetAsInt64(1,0);
  32161. finally
  32162. T.Free;
  32163. end else
  32164. Result := -1;
  32165. end;
  32166. function TSQLRest.TableHasRows(Table: TSQLRecordClass): boolean;
  32167. var T: TSQLTableJSON;
  32168. begin
  32169. if (self=nil) or (Table=nil) then
  32170. T := nil else
  32171. T := ExecuteList([Table],'SELECT RowID FROM '+Table.RecordProps.SQLTableName+' LIMIT 1');
  32172. if T<>nil then
  32173. try
  32174. Result := T.fRowCount>0;
  32175. finally
  32176. T.Free;
  32177. end else
  32178. Result := false;
  32179. end;
  32180. function TSQLRest.TableMaxID(Table: TSQLRecordClass): TID;
  32181. var T: TSQLTableJSON;
  32182. begin
  32183. if (self=nil) or (Table=nil) then
  32184. T := nil else
  32185. T := ExecuteList([Table],'SELECT max(RowID) FROM '+Table.RecordProps.SQLTableName);
  32186. if T<>nil then
  32187. try
  32188. Result := T.GetAsInt64(1,0);
  32189. finally
  32190. T.Free;
  32191. end else
  32192. Result := -1;
  32193. end;
  32194. function TSQLRest.ExecuteList(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): TSQLTableJSON;
  32195. var JSON: RawUTF8;
  32196. begin
  32197. JSON := EngineList(SQL,false);
  32198. if JSON<>'' then
  32199. result := TSQLTableJSON.CreateFromTables(Tables,SQL,JSON) else
  32200. result := nil;
  32201. end;
  32202. function TSQLRest.ExecuteJson(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): RawJSON;
  32203. begin
  32204. result := EngineList(SQL,false);
  32205. end;
  32206. function TSQLRest.Execute(const aSQL: RawUTF8): boolean;
  32207. begin
  32208. result := EngineExecute(aSQL);
  32209. end;
  32210. function TSQLRest.ExecuteFmt(const SQLFormat: RawUTF8;
  32211. const Args: array of const): boolean;
  32212. var SQL: RawUTF8;
  32213. begin
  32214. FormatUTF8(SQLFormat,Args,SQL);
  32215. result := EngineExecute(SQL);
  32216. end;
  32217. function TSQLRest.ExecuteFmt(const SQLFormat: RawUTF8;
  32218. const Args, Bounds: array of const): boolean;
  32219. var SQL: RawUTF8;
  32220. begin
  32221. SQL := FormatUTF8(SQLFormat,Args,Bounds);
  32222. result := EngineExecute(SQL);
  32223. end;
  32224. function TSQLRest.MainFieldValue(Table: TSQLRecordClass; ID: TID;
  32225. ReturnFirstIfNoUnique: boolean=false): RawUTF8;
  32226. begin
  32227. if (self=nil) or (Table=nil) or (ID<=0) then
  32228. result := '' else begin
  32229. result := Table.RecordProps.MainFieldName(ReturnFirstIfNoUnique);
  32230. if result<>'' then
  32231. result := OneFieldValue(Table,Result,ID);
  32232. end;
  32233. end;
  32234. function TSQLRest.MainFieldID(Table: TSQLRecordClass; const Value: RawUTF8): TID;
  32235. var aMainField: integer;
  32236. begin
  32237. result := 0;
  32238. if (self<>nil) and (Value<>'') and (Table<>nil) then
  32239. with Table.RecordProps do begin
  32240. aMainField := MainField[false];
  32241. if aMainField>=0 then
  32242. SetID(OneFieldValue(Table,'RowID',
  32243. Fields.List[aMainField].Name+'=:('+QuotedStr(Value,'''')+'):'),result);
  32244. end;
  32245. end;
  32246. function TSQLRest.MainFieldIDs(Table: TSQLRecordClass; const Values: array of RawUTF8;
  32247. out IDs: TIDDynArray): boolean;
  32248. var aMainField, id: TID;
  32249. begin
  32250. if (self<>nil) and (high(Values)>=0) and (Table<>nil) then
  32251. if high(Values)=0 then begin // handle special case of one Values[] item
  32252. id := MainFieldID(Table,Values[0]);
  32253. if id>0 then begin
  32254. SetLength(IDs,1);
  32255. IDs[0] := id;
  32256. end;
  32257. end else
  32258. with Table.RecordProps do begin // request all Values[] IDs at once
  32259. aMainField := MainField[false];
  32260. if aMainField>=0 then
  32261. OneFieldValues(Table,'RowID',Fields.List[aMainField].Name+' in ('+
  32262. RawUTF8ArrayToQuotedCSV(Values)+')',TInt64DynArray(IDs));
  32263. end;
  32264. result := IDs<>nil;
  32265. end;
  32266. function TSQLRest.FTSMatch(Table: TSQLRecordFTS3Class;
  32267. const WhereClause: RawUTF8; var DocID: TIDDynArray): boolean;
  32268. begin // FTS3 tables don't have any ID, but RowID or DocID
  32269. result := OneFieldValues(Table,'RowID',WhereClause,TInt64DynArray(DocID));
  32270. end;
  32271. function TSQLRest.FTSMatch(Table: TSQLRecordFTS3Class;
  32272. const MatchClause: RawUTF8; var DocID: TIDDynArray;
  32273. const PerFieldWeight: array of double; limit,offset: integer): boolean;
  32274. var WhereClause: RawUTF8;
  32275. i: integer;
  32276. begin
  32277. result := false;
  32278. with Table.RecordProps do
  32279. if length(PerFieldWeight)<>length(SimpleFields) then
  32280. exit else
  32281. WhereClause := FormatUTF8('% MATCH ? ORDER BY rank(matchinfo(%)',
  32282. [SQLTableName,SQLTableName],[MatchClause]);
  32283. for i := 0 to high(PerFieldWeight) do
  32284. WhereClause := FormatUTF8('%,?',[WhereClause],[PerFieldWeight[i]]);
  32285. WhereClause := WhereClause+') DESC';
  32286. if limit>0 then
  32287. WhereClause := FormatUTF8('% LIMIT % OFFSET %',[WhereClause,limit,offset]);
  32288. result := FTSMatch(Table,WhereClause,DocID);
  32289. end;
  32290. function TSQLRest.GetServerTimeStamp: TTimeLog;
  32291. var Tix: cardinal;
  32292. begin
  32293. Tix := GetTickCount shr 9; // resolution change 1 ms -> 512 ms
  32294. if fServerTimeStampCacheTix=Tix then
  32295. result := fServerTimeStampCacheValue.Value else begin
  32296. fServerTimeStampCacheTix := Tix;
  32297. fServerTimeStampCacheValue.From(NowUTC+fServerTimeStampOffset);
  32298. result := fServerTimeStampCacheValue.Value;
  32299. end;
  32300. end;
  32301. procedure TSQLRest.SetServerTimeStamp(const Value: TTimeLog);
  32302. begin
  32303. fServerTimeStampOffset := PTimeLogBits(@Value)^.ToDateTime-NowUTC;
  32304. if fServerTimeStampOffset=0 then
  32305. fServerTimeStampOffset := 0.000001; // retrieve server date/time only once
  32306. end;
  32307. function TSQLRest.GetCache: TSQLRestCache;
  32308. begin
  32309. if self=nil then
  32310. result := nil else begin
  32311. if fCache=nil then
  32312. fCache := TSQLRestCache.Create(self);
  32313. result := fCache;
  32314. end;
  32315. end;
  32316. function TSQLRest.CacheOrNil: TSQLRestCache;
  32317. begin
  32318. if self=nil then
  32319. result := nil else
  32320. result := fCache;
  32321. end;
  32322. function TSQLRest.CacheWorthItForTable(aTableIndex: cardinal): boolean;
  32323. begin
  32324. result := true; // always worth caching by default
  32325. end;
  32326. procedure TSQLRest.BeginCurrentThread(Sender: TThread);
  32327. begin // nothing do to at this level -> see TSQLRestServer.BeginCurrentThread
  32328. end;
  32329. procedure TSQLRest.EndCurrentThread(Sender: TThread);
  32330. begin // most would be done e.g. in TSQLRestServer.EndCurrentThread
  32331. {$ifdef WITHLOG}
  32332. fLogClass.Add.NotifyThreadEnded;
  32333. {$endif}
  32334. end;
  32335. function TSQLRest.GetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand): TSQLRestServerAcquireMode;
  32336. begin
  32337. result := fAcquireExecution[Cmd].Mode;
  32338. end;
  32339. procedure TSQLRest.SetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand; Value: TSQLRestServerAcquireMode);
  32340. begin
  32341. fAcquireExecution[Cmd].Mode := Value;
  32342. end;
  32343. function TSQLRest.GetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand): cardinal;
  32344. begin
  32345. result := fAcquireExecution[Cmd].LockedTimeOut;
  32346. end;
  32347. procedure TSQLRest.SetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand; Value: cardinal);
  32348. begin
  32349. fAcquireExecution[Cmd].LockedTimeOut := Value;
  32350. end;
  32351. function TSQLRest.InternalBatchStart(Method: TSQLURIMethod;
  32352. BatchOptions: TSQLRestBatchOptions): boolean;
  32353. begin
  32354. result := false;
  32355. end;
  32356. procedure TSQLRest.InternalBatchStop;
  32357. begin
  32358. raise EORMException.CreateUTF8('Unexpected %.InternalBatchStop',[self]);
  32359. end;
  32360. function TSQLRest.EngineBatchSend(Table: TSQLRecordClass; const Data: RawUTF8;
  32361. var Results: TIDDynArray; ExpectedResultsCount: integer): integer;
  32362. begin
  32363. raise EORMException.CreateUTF8('BATCH not supported by %',[self]);
  32364. end;
  32365. {$ifdef ISDELPHI2010} // Delphi 2009 generics support is buggy :(
  32366. function TSQLRest.Service<T>: T;
  32367. var service: TServiceFactory;
  32368. begin
  32369. service := fServices.Info(TypeInfo(T));
  32370. if (service=nil) or not service.Get(result) then
  32371. result := Default(T);
  32372. end;
  32373. function TSQLRest.RetrieveList<T>(const aCustomFieldsCSV: RawUTF8): TObjectList<T>;
  32374. begin
  32375. result := RetrieveList<T>('',[],aCustomFieldsCSV);
  32376. end;
  32377. function TSQLRest.RetrieveList<T>(const FormatSQLWhere: RawUTF8;
  32378. const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): TObjectList<T>;
  32379. var Table: TSQLTable;
  32380. begin
  32381. result := nil;
  32382. if self=nil then
  32383. exit;
  32384. Table := MultiFieldValues(TSQLRecordClass(T),aCustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere);
  32385. if Table<>nil then
  32386. try
  32387. result := Table.ToObjectList<T>;
  32388. finally
  32389. Table.Free;
  32390. end;
  32391. end;
  32392. {$endif}
  32393. { TSQLRestCacheEntry }
  32394. procedure TSQLRestCacheEntry.Init;
  32395. begin
  32396. Value.InitSpecific(TypeInfo(TSQLRestCacheEntryValueDynArray),
  32397. Values,djInt64,@Count); // will search/sort by first ID: TID field
  32398. Mutex.Init;
  32399. end;
  32400. procedure TSQLRestCacheEntry.Done;
  32401. begin
  32402. Mutex.Done;
  32403. end;
  32404. procedure TSQLRestCacheEntry.Clear;
  32405. begin
  32406. Mutex.Lock;
  32407. try
  32408. Value.Clear;
  32409. CacheAll := false;
  32410. CacheEnable := false;
  32411. TimeOutMS := 0;
  32412. finally
  32413. Mutex.UnLock;
  32414. end;
  32415. end;
  32416. procedure TSQLRestCacheEntry.FlushCacheEntry(Index: Integer);
  32417. begin
  32418. if cardinal(Index)<cardinal(Count) then
  32419. if CacheAll then
  32420. Value.FastDeleteSorted(Index) else
  32421. with Values[Index] do begin
  32422. TimeStamp64 := 0;
  32423. JSON := '';
  32424. end;
  32425. end;
  32426. procedure TSQLRestCacheEntry.FlushCacheAllEntries;
  32427. var i: integer;
  32428. begin
  32429. if not CacheEnable then
  32430. exit;
  32431. Mutex.Lock;
  32432. try
  32433. if CacheAll then
  32434. Value.Clear else
  32435. for i := 0 to Count-1 do
  32436. with Values[i] do begin
  32437. TimeStamp64 := 0;
  32438. JSON := '';
  32439. end;
  32440. finally
  32441. Mutex.UnLock;
  32442. end;
  32443. end;
  32444. procedure TSQLRestCacheEntry.SetCache(aID: TID);
  32445. var Rec: TSQLRestCacheEntryValue;
  32446. i: integer;
  32447. begin
  32448. Mutex.Lock;
  32449. try
  32450. CacheEnable := true;
  32451. if (not CacheAll) and (not Value.FastLocateSorted(aID,i)) and (i>=0) then begin
  32452. Rec.ID := aID;
  32453. Rec.TimeStamp64 := 0; // indicates no value cache yet
  32454. Value.FastAddSorted(i,Rec);
  32455. end; // do nothing if aID is already in Values[]
  32456. finally
  32457. Mutex.UnLock;
  32458. end;
  32459. end;
  32460. procedure TSQLRestCacheEntry.SetJSON(aID: TID; const aJSON: RawUTF8);
  32461. var Rec: TSQLRestCacheEntryValue;
  32462. i: integer;
  32463. begin
  32464. Rec.ID := aID;
  32465. Rec.TimeStamp64 := GetTickCount64;
  32466. Rec.JSON := aJSON;
  32467. Mutex.Lock;
  32468. try
  32469. if Value.FastLocateSorted(Rec,i) then
  32470. Values[i] := Rec else
  32471. if CacheAll and (i>=0) then
  32472. Value.FastAddSorted(i,Rec);
  32473. finally
  32474. Mutex.UnLock;
  32475. end;
  32476. end;
  32477. procedure TSQLRestCacheEntry.SetJSON(aRecord: TSQLRecord);
  32478. begin // soInsert = include all fields
  32479. SetJSON(aRecord.fID,aRecord.GetJSONValues(true,false,soInsert));
  32480. end;
  32481. function TSQLRestCacheEntry.RetrieveJSON(aID: TID; var aJSON: RawUTF8): boolean;
  32482. var i: integer;
  32483. begin
  32484. result := false;
  32485. Mutex.Lock;
  32486. try
  32487. i := Value.Find(aID); // fast binary search by first ID field
  32488. if i>=0 then
  32489. with Values[i] do
  32490. if TimeStamp64<>0 then // 0 when there is no JSON value cached
  32491. if (TimeOutMS<>0) and (GetTickCount64>TimeStamp64+TimeOutMS) then
  32492. FlushCacheEntry(i) else begin
  32493. aJSON := JSON;
  32494. result := true; // found a non outdated serialized value in cache
  32495. end;
  32496. finally
  32497. Mutex.UnLock;
  32498. end;
  32499. end;
  32500. function TSQLRestCacheEntry.RetrieveJSON(aID: TID; aValue: TSQLRecord): boolean;
  32501. var JSON: RawUTF8;
  32502. begin
  32503. if RetrieveJSON(aID,JSON) then begin
  32504. aValue.FillFrom(JSON);
  32505. aValue.fID := aID; // override RowID field (may be not present after Update)
  32506. result := true;
  32507. end else
  32508. result := false;
  32509. end;
  32510. { TSQLRestCache }
  32511. function TSQLRestCache.CachedEntries: cardinal;
  32512. var i,j: integer;
  32513. begin
  32514. result := 0;
  32515. if self<>nil then
  32516. for i := 0 to high(fCache) do
  32517. with fCache[i] do
  32518. if CacheEnable then begin
  32519. Mutex.Lock;
  32520. try
  32521. for j := 0 to Count-1 do
  32522. if Values[j].TimeStamp64<>0 then
  32523. inc(result);
  32524. finally
  32525. Mutex.UnLock;
  32526. end;
  32527. end;
  32528. end;
  32529. function TSQLRestCache.CachedMemory(FlushedEntriesCount: PInteger=nil): cardinal;
  32530. var i,j: integer;
  32531. tix: Int64;
  32532. begin
  32533. result := 0;
  32534. if FlushedEntriesCount<>nil then
  32535. FlushedEntriesCount^ := 0;
  32536. if self<>nil then
  32537. for i := 0 to high(fCache) do
  32538. with fCache[i] do
  32539. if CacheEnable and (Count>0) then begin
  32540. tix := GetTickCount64-TimeOutMS;
  32541. Mutex.Lock;
  32542. try
  32543. for j := Count-1 downto 0 do
  32544. if Values[j].TimeStamp64<>0 then begin
  32545. if (TimeOutMS<>0) and (tix>Values[j].TimeStamp64) then begin
  32546. FlushCacheEntry(j);
  32547. if FlushedEntriesCount<>nil then
  32548. inc(FlushedEntriesCount^);
  32549. end else
  32550. inc(result,length(Values[j].JSON)+(sizeof(Values[j])+16));
  32551. end;
  32552. finally
  32553. Mutex.UnLock;
  32554. end;
  32555. end;
  32556. end;
  32557. function TSQLRestCache.SetTimeOut(aTable: TSQLRecordClass; aTimeoutMS: Cardinal): boolean;
  32558. var i: integer;
  32559. begin
  32560. result := false;
  32561. if (self=nil) or (aTable=nil) then
  32562. exit;
  32563. i := Rest.Model.GetTableIndexExisting(aTable);
  32564. if Rest.CacheWorthItForTable(i) then
  32565. if Cardinal(i)<Cardinal(Length(fCache)) then
  32566. with fCache[i] do begin
  32567. Mutex.Lock;
  32568. try
  32569. TimeOutMS := aTimeOutMS;
  32570. finally
  32571. Mutex.UnLock;
  32572. end;
  32573. result := true;
  32574. end;
  32575. end;
  32576. function TSQLRestCache.IsCached(aTable: TSQLRecordClass): boolean;
  32577. var i: cardinal;
  32578. begin
  32579. result := false;
  32580. if (self=nil) or (aTable=nil) then
  32581. exit;
  32582. i := Rest.Model.GetTableIndexExisting(aTable);
  32583. if i<Cardinal(Length(fCache)) then
  32584. if fCache[i].CacheEnable then
  32585. result := true;
  32586. end;
  32587. function TSQLRestCache.SetCache(aTable: TSQLRecordClass): boolean;
  32588. var i: integer;
  32589. begin
  32590. result := false;
  32591. if (self=nil) or (aTable=nil) then
  32592. exit;
  32593. i := Rest.Model.GetTableIndexExisting(aTable);
  32594. if Rest.CacheWorthItForTable(i) then
  32595. if Cardinal(i)<Cardinal(Length(fCache)) then
  32596. with fCache[i] do begin
  32597. // global cache of all records of this table
  32598. Mutex.Lock;
  32599. try
  32600. CacheEnable := true;
  32601. CacheAll := True;
  32602. Value.Clear;
  32603. result := true;
  32604. finally
  32605. Mutex.UnLock;
  32606. end;
  32607. end;
  32608. end;
  32609. function TSQLRestCache.SetCache(aTable: TSQLRecordClass; aID: TID): boolean;
  32610. var i: cardinal;
  32611. begin
  32612. result := false;
  32613. if (self=nil) or (aTable=nil) or (aID<=0) then
  32614. exit;
  32615. i := Rest.Model.GetTableIndex(aTable);
  32616. if i>=cardinal(Length(fCache)) then
  32617. exit;
  32618. if Rest.CacheWorthItForTable(i) then
  32619. fCache[i].SetCache(aID);
  32620. result := True;
  32621. end;
  32622. function TSQLRestCache.SetCache(aTable: TSQLRecordClass; const aIDs: array of TID): boolean;
  32623. var i: cardinal;
  32624. j: integer;
  32625. begin
  32626. result := false;
  32627. if (self=nil) or (aTable=nil) or (length(aIDs)=0) then
  32628. exit;
  32629. i := Rest.Model.GetTableIndex(aTable);
  32630. if i>=cardinal(Length(fCache)) then
  32631. exit;
  32632. if Rest.CacheWorthItForTable(i) then
  32633. for j := 0 to high(aIDs) do
  32634. fCache[i].SetCache(aIDs[j]);
  32635. result := True;
  32636. end;
  32637. function TSQLRestCache.SetCache(aRecord: TSQLRecord): boolean;
  32638. begin
  32639. if (self=nil) or (aRecord=nil) or (aRecord.fID<=0) then
  32640. result := false else
  32641. result := SetCache(PSQLRecordClass(aRecord)^,aRecord.fID);
  32642. end;
  32643. constructor TSQLRestCache.Create(aRest: TSQLRest);
  32644. var i: integer;
  32645. begin
  32646. if aRest=nil then
  32647. EBusinessLayerException.CreateUTF8('%.Create',[self]);
  32648. fRest := aRest;
  32649. SetLength(fCache,length(fRest.Model.Tables));
  32650. for i := 0 to high(fCache) do
  32651. fCache[i].Init;
  32652. end;
  32653. destructor TSQLRestCache.Destroy;
  32654. var i: integer;
  32655. begin
  32656. for i := 0 to high(fCache) do
  32657. fCache[i].Done;
  32658. inherited;
  32659. end;
  32660. procedure TSQLRestCache.Clear;
  32661. var i: integer;
  32662. begin
  32663. if self<>nil then
  32664. for i := 0 to high(fCache) do
  32665. fCache[i].Clear;
  32666. end;
  32667. procedure TSQLRestCache.Flush;
  32668. var i: integer;
  32669. begin
  32670. if self<>nil then
  32671. for i := 0 to high(fCache) do
  32672. fCache[i].FlushCacheAllEntries; // include *CriticalSection(Mutex)
  32673. end;
  32674. procedure TSQLRestCache.Flush(aTable: TSQLRecordClass);
  32675. begin
  32676. if self<>nil then // includes *CriticalSection(Mutex):
  32677. fCache[fRest.Model.GetTableIndexExisting(aTable)].FlushCacheAllEntries;
  32678. end;
  32679. procedure TSQLRestCache.Flush(aTable: TSQLRecordClass; aID: TID);
  32680. begin
  32681. if self<>nil then
  32682. with fCache[fRest.Model.GetTableIndexExisting(aTable)] do
  32683. if CacheEnable then begin
  32684. Mutex.Lock;
  32685. try
  32686. FlushCacheEntry(Value.Find(aID));
  32687. finally
  32688. Mutex.UnLock;
  32689. end;
  32690. end;
  32691. end;
  32692. procedure TSQLRestCache.Flush(aTable: TSQLRecordClass; const aIDs: array of TID);
  32693. var i: integer;
  32694. begin
  32695. if (self<>nil) and (length(aIDs)>0) then
  32696. with fCache[fRest.Model.GetTableIndexExisting(aTable)] do
  32697. if CacheEnable then begin
  32698. Mutex.Lock;
  32699. try
  32700. for i := 0 to high(aIDs) do
  32701. FlushCacheEntry(Value.Find(aIDs[i]));
  32702. finally
  32703. Mutex.UnLock;
  32704. end;
  32705. end;
  32706. end;
  32707. procedure TSQLRestCache.Notify(aTable: TSQLRecordClass; aID: TID;
  32708. const aJSON: RawUTF8; aAction: TSQLOccasion);
  32709. begin
  32710. if (self<>nil) and (aTable<>nil) and (aID>0) then
  32711. Notify(fRest.Model.GetTableIndex(aTable),aID,aJSON,aAction);
  32712. end;
  32713. procedure TSQLRestCache.Notify(aRecord: TSQLRecord; aAction: TSQLOccasion);
  32714. var aTableIndex: cardinal;
  32715. begin
  32716. if (self=nil) or (aRecord=nil) or (aRecord.fID<=0) or
  32717. not (aAction in [soInsert,soUpdate]) then
  32718. exit;
  32719. aTableIndex := fRest.Model.GetTableIndex(PSQLRecordClass(aRecord)^);
  32720. if aTableIndex<Cardinal(Length(fCache)) then
  32721. with fCache[aTableIndex] do
  32722. if CacheEnable then
  32723. SetJSON(aRecord);
  32724. end;
  32725. procedure TSQLRestCache.Notify(aTableIndex: integer; aID: TID;
  32726. const aJSON: RawUTF8; aAction: TSQLOccasion);
  32727. begin
  32728. if (self<>nil) and (aID>0) and (aAction in [soSelect,soInsert,soUpdate]) and
  32729. (aJSON<>'') and (Cardinal(aTableIndex)<Cardinal(Length(fCache))) then
  32730. with fCache[aTableIndex] do
  32731. if CacheEnable then
  32732. SetJSON(aID,aJSON);
  32733. end;
  32734. procedure TSQLRestCache.NotifyDeletion(aTableIndex, aID: TID);
  32735. begin
  32736. if (self<>nil) and (aID>0) and
  32737. (Cardinal(aTableIndex)<Cardinal(Length(fCache))) then
  32738. with fCache[aTableIndex] do
  32739. if CacheEnable then begin
  32740. Mutex.Lock;
  32741. try
  32742. FlushCacheEntry(Value.Find(aID));
  32743. finally
  32744. Mutex.UnLock;
  32745. end;
  32746. end;
  32747. end;
  32748. procedure TSQLRestCache.NotifyDeletion(aTable: TSQLRecordClass; aID: TID);
  32749. begin
  32750. if (self<>nil) and (aTable<>nil) and (aID>0) then
  32751. NotifyDeletion(fRest.Model.GetTableIndex(aTable),aID);
  32752. end;
  32753. function TSQLRestCache.Retrieve(aID: TID; aValue: TSQLRecord): boolean;
  32754. var TableIndex: cardinal;
  32755. begin
  32756. result := false;
  32757. if (self=nil) or (aValue=nil) or (aID<=0) then
  32758. exit;
  32759. TableIndex := fRest.Model.GetTableIndexExisting(PSQLRecordClass(aValue)^);
  32760. if TableIndex<cardinal(Length(fCache)) then
  32761. with fCache[TableIndex] do
  32762. if CacheEnable and RetrieveJSON(aID,aValue) then
  32763. result := true;
  32764. end;
  32765. function TSQLRestCache.Retrieve(aTableIndex, aID: TID): RawUTF8;
  32766. begin
  32767. result := '';
  32768. if (self<>nil) and (aID>0) and
  32769. (Cardinal(aTableIndex)<Cardinal(Length(fCache))) then
  32770. with fCache[aTableIndex] do
  32771. if CacheEnable then
  32772. RetrieveJSON(aID,result);
  32773. end;
  32774. { TSQLRestThread }
  32775. constructor TSQLRestThread.Create(aRest: TSQLRest;
  32776. aOwnRest, aCreateSuspended: boolean);
  32777. begin
  32778. if aRest=nil then
  32779. raise EORMException.CreateUTF8('%.Create(aRest=nil)',[self]);
  32780. fSafe.Init;
  32781. fRest := aRest;
  32782. fOwnRest := aOwnRest;
  32783. inherited Create(aCreateSuspended);
  32784. end;
  32785. destructor TSQLRestThread.Destroy;
  32786. begin
  32787. inherited Destroy;
  32788. if fOwnRest then
  32789. FreeAndNil(fRest);
  32790. fSafe.Done;
  32791. end;
  32792. function TSQLRestThread.SleepOrTerminated(MS: integer): boolean;
  32793. var endtix: Int64;
  32794. begin
  32795. result := true; // notify Terminated
  32796. if Terminated then
  32797. exit;
  32798. if MS<32 then begin // smaller than GetTickCount resolution (under Windows)
  32799. sleep(MS);
  32800. if Terminated then
  32801. exit;
  32802. end else begin
  32803. endtix := GetTickCount64+MS;
  32804. repeat
  32805. sleep(10);
  32806. if Terminated then
  32807. exit;
  32808. until GetTickCount64>endtix;
  32809. end;
  32810. result := false; // normal delay expiration
  32811. end;
  32812. procedure TSQLRestThread.Execute;
  32813. begin
  32814. {$ifdef WITHLOG}
  32815. fLog := FRest.LogClass.Add;
  32816. {$endif}
  32817. SetCurrentThreadName('% %',[self,fRest.Model.Root]);
  32818. FRest.BeginCurrentThread(self);
  32819. try
  32820. try
  32821. InternalExecute;
  32822. except
  32823. on E: Exception do
  32824. {$ifdef WITHLOG}
  32825. fLog.Add.Log(sllError,'Unhandled % in %.Execute -> abort',[E,ClassType],self);
  32826. {$endif}
  32827. end;
  32828. finally
  32829. FRest.EndCurrentThread(self);
  32830. end;
  32831. end;
  32832. {$ifndef HASTTHREADSTART}
  32833. procedure TSQLRestThread.Start;
  32834. begin
  32835. Resume;
  32836. end;
  32837. {$endif}
  32838. { TSQLRestURIParams }
  32839. procedure TSQLRestURIParams.Init;
  32840. begin
  32841. OutStatus := 0;
  32842. OutInternalState := 0;
  32843. RestAccessRights := nil;
  32844. LowLevelConnectionID := 0;
  32845. byte(LowLevelFlags) := 0;
  32846. end;
  32847. procedure TSQLRestURIParams.Init(const aURI,aMethod,aInHead,aInBody: RawUTF8);
  32848. begin
  32849. Init;
  32850. Url := aURI;
  32851. Method := aMethod;
  32852. InHead := aInHead;
  32853. InBody := aInBody;
  32854. end;
  32855. function TSQLRestURIParams.InBodyType(GuessJSONIfNoneSet: boolean): RawUTF8;
  32856. begin
  32857. result := FindIniNameValue(pointer(InHead),HEADER_CONTENT_TYPE_UPPER);
  32858. if GuessJSONIfNoneSet and (result='') then
  32859. result := JSON_CONTENT_TYPE_VAR;
  32860. end;
  32861. function TSQLRestURIParams.OutBodyType(GuessJSONIfNoneSet: boolean): RawUTF8;
  32862. begin
  32863. result := FindIniNameValue(pointer(OutHead),HEADER_CONTENT_TYPE_UPPER);
  32864. if GuessJSONIfNoneSet and (result='') then
  32865. result := JSON_CONTENT_TYPE_VAR;
  32866. end;
  32867. { TSQLRestClientCallbacks }
  32868. constructor TSQLRestClientCallbacks.Create(aOwner: TSQLRestClientURI);
  32869. begin
  32870. inherited Create;
  32871. Owner := aOwner;
  32872. end;
  32873. function TSQLRestClientCallbacks.FindIndex(aID: integer): integer;
  32874. begin
  32875. if self<>nil then
  32876. for result := 0 to Count-1 do
  32877. if List[result].ID=aID then
  32878. exit;
  32879. result := -1;
  32880. end;
  32881. function TSQLRestClientCallbacks.FindEntry(var aItem: TSQLRestClientCallbackItem): boolean;
  32882. var i: Integer;
  32883. P: PSQLRestClientCallbackItem;
  32884. begin
  32885. result := false;
  32886. if self=nil then
  32887. exit;
  32888. fSafe.Lock;
  32889. try
  32890. P := pointer(List);
  32891. for i := 1 to Count do
  32892. if P^.ID=aItem.ID then begin
  32893. if P^.Instance<>nil then begin
  32894. result := true;
  32895. aItem := P^;
  32896. end;
  32897. exit;
  32898. end else
  32899. inc(P);
  32900. finally
  32901. Safe.UnLock;
  32902. end;
  32903. end;
  32904. function TSQLRestClientCallbacks.FindAndRelease(aID: integer): boolean;
  32905. var i: Integer;
  32906. begin
  32907. result := false;
  32908. if self=nil then
  32909. exit;
  32910. fSafe.Lock;
  32911. try
  32912. i := FindIndex(aID);
  32913. if i<0 then
  32914. exit;
  32915. List[i].ReleasedFromServer := True;
  32916. finally
  32917. Safe.UnLock;
  32918. end;
  32919. result := true;
  32920. end;
  32921. function TSQLRestClientCallbacks.UnRegisterByIndex(index: integer): boolean;
  32922. begin
  32923. result := false;
  32924. if cardinal(index)>=cardinal(Count) then
  32925. exit;
  32926. with List[index] do
  32927. if not ReleasedFromServer then
  32928. try
  32929. if Owner.FakeCallbackUnregister(Factory,ID,Instance) then
  32930. result := true;
  32931. except
  32932. // ignore errors at this point, and continue
  32933. end;
  32934. dec(Count);
  32935. if index<Count then
  32936. MoveFast(List[index+1],List[index],(Count-index)*sizeof(List[index]));
  32937. end;
  32938. function TSQLRestClientCallbacks.UnRegister(aInstance: pointer): boolean;
  32939. var i: integer;
  32940. begin
  32941. result := false;
  32942. if (self=nil) or (Count=0) then
  32943. exit;
  32944. Safe.Lock;
  32945. try
  32946. for i := Count-1 downto 0 do
  32947. if List[i].Instance=aInstance then
  32948. if UnRegisterByIndex(i) then
  32949. result := true else
  32950. break;
  32951. finally
  32952. Safe.UnLock;
  32953. end;
  32954. end;
  32955. procedure TSQLRestClientCallbacks.DoRegister(aID: integer;
  32956. aInstance: pointer; aFactory: TInterfaceFactory);
  32957. begin
  32958. if aID<=0 then
  32959. exit;
  32960. Safe.Lock;
  32961. try
  32962. if length(List)>=Count then
  32963. SetLength(List,Count+32);
  32964. with List[Count] do begin
  32965. ID := aID;
  32966. Instance := aInstance;
  32967. Factory := aFactory;
  32968. end;
  32969. inc(Count);
  32970. finally
  32971. Safe.UnLock;
  32972. end;
  32973. end;
  32974. function TSQLRestClientCallbacks.DoRegister(aInstance: pointer;
  32975. aFactory: TInterfaceFactory): integer;
  32976. begin
  32977. result := InterlockedIncrement(fCurrentID);
  32978. DoRegister(result,aInstance,aFactory);
  32979. end;
  32980. { TSQLRestClientURI }
  32981. function TSQLRestClientURI.EngineExecute(const SQL: RawUTF8): boolean;
  32982. begin
  32983. result := URI(Model.Root,'POST',nil,nil,@SQL).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  32984. end;
  32985. function TSQLRestClientURI.URIGet(Table: TSQLRecordClass; ID: TID;
  32986. var Resp: RawUTF8; ForUpdate: boolean=false): Int64Rec;
  32987. const METHOD: array[boolean] of RawUTF8 = ('GET','LOCK');
  32988. begin
  32989. result := URI(Model.getURIID(Table,ID),METHOD[ForUpdate],@Resp,nil,nil);
  32990. end;
  32991. function TSQLRestClientURI.UnLock(Table: TSQLRecordClass; aID: TID): boolean;
  32992. begin
  32993. if (self=nil) or not Model.UnLock(Table,aID) then
  32994. result := false else // was not locked by the client
  32995. result := URI(Model.getURIID(Table,aID),'UNLOCK').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  32996. end;
  32997. function TSQLRestClientURI.ExecuteList(const Tables: array of TSQLRecordClass;
  32998. const SQL: RawUTF8): TSQLTableJSON;
  32999. var Resp: RawUTF8;
  33000. begin
  33001. if self=nil then
  33002. result := nil else
  33003. with URI(Model.Root,'GET',@Resp,nil,@SQL) do
  33004. if Lo=HTML_SUCCESS then begin // GET with SQL sent
  33005. if high(Tables)=0 then
  33006. result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp) else
  33007. result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp);
  33008. result.fInternalState := Hi;
  33009. end else // get data
  33010. result := nil;
  33011. end;
  33012. function TSQLRestClientURI.ServerInternalState: cardinal;
  33013. begin
  33014. if (Self=nil) or (Model=nil) then // avoid GPF
  33015. result := cardinal(-1) else
  33016. result := URI(Model.Root,'STATE').Hi;
  33017. end;
  33018. function TSQLRestClientURI.ServerCacheFlush(aTable: TSQLRecordClass; aID: TID): boolean;
  33019. var aResp: RawUTF8;
  33020. begin
  33021. if (Self=nil) or (Model=nil) then // avoid GPF
  33022. result := false else
  33023. result := CallBackGet('CacheFlush',[],aResp,aTable,aID) in [HTML_SUCCESS,HTML_NOCONTENT];
  33024. end;
  33025. function TSQLRestClientURI.ServerTimeStampSynchronize: boolean;
  33026. var status: integer;
  33027. aResp: RawUTF8;
  33028. begin
  33029. if self=nil then begin
  33030. result := false;
  33031. exit;
  33032. end;
  33033. fServerTimeStampOffset := 0.0001; // avoid endless recursive call
  33034. status := CallBackGet('TimeStamp',[],aResp);
  33035. result := (status=HTML_SUCCESS) and (aResp<>'');
  33036. if result then
  33037. SetServerTimeStamp(GetInt64(pointer(aResp))) else begin
  33038. InternalLog('/TimeStamp call failed -> Server not available',sllWarning);
  33039. fLastErrorMessage := 'Server not available - '+Trim(fLastErrorMessage);
  33040. end;
  33041. end;
  33042. function TSQLRestClientURI.InternalRemoteLogSend(const aText: RawUTF8): boolean;
  33043. begin
  33044. result := URI(Model.getURICallBack('RemoteLog',nil,0),
  33045. 'PUT',nil,nil,@aText).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  33046. end;
  33047. {$ifdef MSWINDOWS}
  33048. type
  33049. TSQLRestClientURIServiceNotification = class(TServiceMethodExecute)
  33050. protected
  33051. fOwner: TSQLRestClientURI;
  33052. fInstance: TObject;
  33053. fPar: RawUTF8;
  33054. end;
  33055. procedure TSQLRestClientURI.ServiceNotificationMethodViaMessages(hWnd: HWND; Msg: UINT);
  33056. begin
  33057. if Msg=0 then
  33058. hWnd := 0; // avoid half defined parameters
  33059. fServiceNotificationMethodViaMessages.Wnd := hWnd;
  33060. fServiceNotificationMethodViaMessages.Msg := Msg;
  33061. end;
  33062. class procedure TSQLRestClientURI.ServiceNotificationMethodExecute(var Msg : TMessage);
  33063. var exec: TSQLRestClientURIServiceNotification;
  33064. begin
  33065. exec := pointer(Msg.LParam);
  33066. if exec<>nil then
  33067. try
  33068. try
  33069. if exec.InheritsFrom(TSQLRestClientURIServiceNotification) and
  33070. (HWND(Msg.WParam)=exec.fOwner.fServiceNotificationMethodViaMessages.Wnd) then
  33071. // run asynchronous notification callback in the main UI thread context
  33072. exec.ExecuteJson([exec.fInstance],pointer(exec.fPar),nil);
  33073. finally
  33074. exec.Free; // always release notification resources
  33075. end;
  33076. except
  33077. ; // ignore any exception for this asynchronous callback execution
  33078. end;
  33079. end;
  33080. {$endif MSWINDOWS}
  33081. type
  33082. TServiceInternalMethod = (imFree, imContract, imSignature);
  33083. const
  33084. SERVICE_PSEUDO_METHOD: array[TServiceInternalMethod] of RawUTF8 = (
  33085. '_free_','_contract_','_signature_');
  33086. SERVICE_PSEUDO_METHOD_COUNT = Length(SERVICE_PSEUDO_METHOD);
  33087. procedure TSQLRestClientURI.InternalNotificationMethodExecute(
  33088. var Ctxt: TSQLRestURIParams);
  33089. var url,root,interfmethod,interf,id,method,frames: RawUTF8;
  33090. callback: TSQLRestClientCallbackItem;
  33091. methodIndex: integer;
  33092. WR: TTextWriter;
  33093. ok: Boolean;
  33094. procedure Call(methodIndex: Integer; const par: RawUTF8; res: TTextWriter);
  33095. var method: PServiceMethod;
  33096. exec: TServiceMethodExecute;
  33097. begin
  33098. method := @callback.Factory.Methods[methodIndex];
  33099. {$ifdef MSWINDOWS}
  33100. if (fServiceNotificationMethodViaMessages.Wnd<>0) and
  33101. (method^.ArgsOutputValuesCount=0) then begin
  33102. // expects no output -> asynchronous non blocking notification in UI thread
  33103. Ctxt.OutStatus := 0;
  33104. exec := TSQLRestClientURIServiceNotification.Create(method);
  33105. TSQLRestClientURIServiceNotification(exec).fOwner := self;
  33106. TSQLRestClientURIServiceNotification(exec).fInstance := callback.Instance;
  33107. TSQLRestClientURIServiceNotification(exec).fPar := par;
  33108. with fServiceNotificationMethodViaMessages do
  33109. ok := PostMessage(Wnd,Msg,Wnd,LPARAM(exec));
  33110. if ok then
  33111. exit;
  33112. end else // if PostMessage() failed (e.g. invalid Wnd/Msg) -> blocking exec
  33113. {$endif}
  33114. exec := TServiceMethodExecute.Create(method);
  33115. try
  33116. ok := exec.ExecuteJson([callback.Instance],pointer(par),res);
  33117. Ctxt.OutHead := exec.ServiceCustomAnswerHead;
  33118. Ctxt.OutStatus := exec.ServiceCustomAnswerStatus;
  33119. finally
  33120. exec.Free;
  33121. end;
  33122. end;
  33123. begin
  33124. Ctxt.OutStatus := HTML_BADREQUEST;
  33125. url := Ctxt.Url;
  33126. if url='' then
  33127. exit;
  33128. if url[1]='/' then
  33129. system.delete(url,1,1);
  33130. Split(Split(url,'/',root),'/',interfmethod,id); // 'root/BidirCallback.AsynchEvent/1'
  33131. if not IdemPropNameU(root,Model.Root) then
  33132. exit;
  33133. callback.ID := GetInteger(pointer(id));
  33134. if callback.ID<=0 then
  33135. exit;
  33136. if interfmethod=SERVICE_PSEUDO_METHOD[imFree] then begin
  33137. if fFakeCallbacks.FindAndRelease(callback.ID) then
  33138. Ctxt.OutStatus := HTML_SUCCESS;
  33139. exit;
  33140. end;
  33141. if not fFakeCallbacks.FindEntry(callback) then
  33142. exit;
  33143. if (Ctxt.InHead<>'') and
  33144. (callback.Factory.MethodIndexCurrentFrameCallback>=0) then begin
  33145. frames := FindIniNameValue(pointer(Ctxt.InHead),'SEC-WEBSOCKET-FRAME: ');
  33146. end;
  33147. split(interfmethod,'.',interf,method);
  33148. methodIndex := callback.Factory.FindMethodIndex(method);
  33149. if methodIndex<0 then
  33150. exit;
  33151. if IdemPropNameU(interfmethod,callback.Factory.Methods[methodIndex].InterfaceDotMethodName) then
  33152. try
  33153. WR := TJSONSerializer.CreateOwnedStream;
  33154. try
  33155. WR.AddShort('{"result":[');
  33156. if frames='[0]' then // call before the first method of the jumbo frame
  33157. Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil);
  33158. Call(methodIndex,Ctxt.InBody,WR);
  33159. if ok then begin
  33160. if Ctxt.OutHead='' then begin // <>'' if set via TServiceCustomAnswer
  33161. WR.Add(']','}');
  33162. Ctxt.OutStatus := HTML_SUCCESS;
  33163. end;
  33164. Ctxt.OutBody := WR.Text;
  33165. end else
  33166. Ctxt.OutStatus := HTML_SERVERERROR;
  33167. if frames='[1]' then // call after the last method of the jumbo frame
  33168. Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil);
  33169. finally
  33170. WR.Free;
  33171. end;
  33172. except
  33173. on E: Exception do begin
  33174. Ctxt.OutHead := '';
  33175. Ctxt.OutBody := ObjectToJSONDebug(E);
  33176. Ctxt.OutStatus := HTML_SERVERERROR;
  33177. end;
  33178. end;
  33179. end;
  33180. {$ifdef LVCL} // SyncObjs.TEvent not available in LVCL yet
  33181. function TSQLRestClientURI.ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo;
  33182. const Text: RawUTF8): boolean;
  33183. begin
  33184. result := InternalRemoteLogSend(Text);
  33185. end;
  33186. {$else}
  33187. type
  33188. TRemoteLogThread = class(TSQLRestThread)
  33189. protected
  33190. fClient: TSQLRestClientURI;
  33191. fPendingRows: RawUTF8;
  33192. fNotifier: TEvent;
  33193. procedure InternalExecute; override;
  33194. public
  33195. constructor Create(aClient: TSQLRestClientURI); reintroduce;
  33196. destructor Destroy; override;
  33197. procedure AddRow(const aText: RawUTF8);
  33198. end;
  33199. constructor TRemoteLogThread.Create(aClient: TSQLRestClientURI);
  33200. begin
  33201. fNotifier := TEvent.Create(nil,false,false,'');
  33202. fClient := aClient;
  33203. inherited Create(aClient,false,false);
  33204. end;
  33205. destructor TRemoteLogThread.Destroy;
  33206. var i: integer;
  33207. begin
  33208. if fPendingRows<>'' then begin
  33209. fNotifier.SetEvent;
  33210. for i := 1 to 200 do begin
  33211. SleepHiRes(10);
  33212. if fPendingRows='' then
  33213. break;
  33214. end;
  33215. end;
  33216. Terminate; // will notify Execute that the process is finished
  33217. fNotifier.SetEvent;
  33218. SleepHiRes(50); // wait for Execute to finish
  33219. fNotifier.Free;
  33220. inherited;
  33221. end;
  33222. procedure TRemoteLogThread.AddRow(const aText: RawUTF8);
  33223. begin
  33224. fSafe.Lock;
  33225. try
  33226. if fPendingRows='' then
  33227. fPendingRows := aText else
  33228. fPendingRows := fPendingRows+#13#10+aText;
  33229. finally
  33230. fSafe.UnLock;
  33231. end;
  33232. fNotifier.SetEvent;
  33233. end;
  33234. procedure TRemoteLogThread.InternalExecute;
  33235. var aText: RawUTF8;
  33236. begin
  33237. while not Terminated do
  33238. if FixedWaitFor(fNotifier,INFINITE)=wrSignaled then begin
  33239. if Terminated then
  33240. break;
  33241. fSafe.Lock;
  33242. try
  33243. aText := fPendingRows;
  33244. fPendingRows := '';
  33245. finally
  33246. fSafe.UnLock;
  33247. end;
  33248. if (aText<>'') and not Terminated then
  33249. try
  33250. while not fClient.InternalRemoteLogSend(aText) do
  33251. if SleepOrTerminated(2000) then // retry after 2 seconds delay
  33252. exit;
  33253. except
  33254. on E: Exception do
  33255. if (fClient<>nil) and not Terminated then
  33256. fClient.InternalLog('%.Execute fatal error: %'+
  33257. 'some events were not transmitted',[ClassType,E],sllWarning);
  33258. end;
  33259. end;
  33260. end;
  33261. function TSQLRestClientURI.ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo;
  33262. const Text: RawUTF8): boolean;
  33263. begin
  33264. if fRemoteLogThread=nil then
  33265. result := InternalRemoteLogSend(Text) else begin
  33266. TRemoteLogThread(fRemoteLogThread).AddRow(Text);
  33267. result := true;
  33268. end;
  33269. end;
  33270. {$endif LVCL}
  33271. function TSQLRestClientURI.ServerRemoteLog(Level: TSynLogInfo;
  33272. FormatMsg: PUTF8Char; const Args: array of const): boolean;
  33273. begin
  33274. result := ServerRemoteLog(nil,Level,
  33275. FormatUTF8('%00% %',[NowToString(false),LOG_LEVEL_TEXT[Level],
  33276. FormatUTF8(FormatMsg,Args)]));
  33277. end;
  33278. procedure TSQLRestClientURI.ServerRemoteLogStart(aLogClass: TSynLogClass;
  33279. aClientOwnedByFamily: boolean);
  33280. begin
  33281. if (fRemoteLogClass<>nil) or (aLogClass=nil) then
  33282. exit;
  33283. {$ifdef WITHLOG}
  33284. SetLogClass(TSynLog.Void); // this client won't log anything
  33285. {$endif}
  33286. if not ServerRemoteLog(sllClient,'Remote Client % Connected',[self]) then
  33287. // first test server without threading
  33288. raise ECommunicationException.CreateUTF8(
  33289. 'Connection to RemoteLog server impossible'#13#10'%',[LastErrorMessage]);
  33290. {$ifndef LVCL}
  33291. if fRemoteLogThread<>nil then
  33292. raise ECommunicationException.CreateUTF8('%.ServerRemoteLogStart twice',[self]);
  33293. fRemoteLogThread := TRemoteLogThread.Create(self);
  33294. {$endif}
  33295. fRemoteLogClass := aLogClass.Add;
  33296. aLogClass.Family.EchoRemoteStart(self,ServerRemoteLog,aClientOwnedByFamily);
  33297. fRemoteLogOwnedByFamily := aClientOwnedByFamily;
  33298. end;
  33299. procedure TSQLRestClientURI.ServerRemoteLogStop;
  33300. begin
  33301. if fRemoteLogClass=nil then
  33302. exit;
  33303. if not fRemoteLogOwnedByFamily then begin
  33304. fRemoteLogClass.Log(sllTrace,'End Echoing to remote server');
  33305. fRemoteLogClass.Family.EchoRemoteStop;
  33306. end;
  33307. fRemoteLogClass := nil;
  33308. end;
  33309. function TSQLRestClientURI.UpdateFromServer(const Data: array of TObject; out Refreshed: boolean;
  33310. PCurrentRow: PInteger): boolean;
  33311. // notes about refresh mechanism:
  33312. // - if server doesn't implement InternalState, its value is 0 -> always refresh
  33313. // - if any TSQLTableJSON or TSQLRecord belongs to a TSQLRestStorage,
  33314. // the Server stated fInternalState=cardinal(-1) for them -> always refresh
  33315. var i: integer;
  33316. State: cardinal;
  33317. Resp: RawUTF8;
  33318. T: TSQLTableJSON;
  33319. TRefreshed: boolean; // to check for each Table refresh
  33320. const TState: array[boolean] of TOnTableUpdateState = (tusNoChange,tusChanged);
  33321. begin
  33322. result := self<>nil;
  33323. Refreshed := false;
  33324. if not result then
  33325. exit; // avoid GPF
  33326. State := ServerInternalState; // get revision state from server
  33327. for i := 0 to high(Data) do
  33328. if Data[i]<>nil then
  33329. if TObject(Data[i]).InheritsFrom(TSQLTableJSON) then begin
  33330. T := TSQLTableJSON((Data[i]));
  33331. if (T.QuerySQL<>'') and (T.InternalState<>State) then begin // refresh needed?
  33332. with URI(Model.Root,'GET',@Resp,nil,@T.QuerySQL) do
  33333. if Lo=HTML_SUCCESS then begin // GET with SQL sent
  33334. if Assigned(OnTableUpdate) then
  33335. OnTableUpdate(T,tusPrepare);
  33336. TRefreshed := false;
  33337. if not T.UpdateFrom(Resp,TRefreshed,PCurrentRow) then
  33338. result := false else // mark error retrieving new content
  33339. T.fInternalState := Hi;
  33340. if TRefreshed then
  33341. Refreshed := true;
  33342. if Assigned(OnTableUpdate) then
  33343. OnTableUpdate(T,TState[TRefreshed]);
  33344. end
  33345. else result := false; // mark error retrieving new content
  33346. end;
  33347. end else
  33348. if TObject(Data[i]).InheritsFrom(TSQLRecord) then
  33349. with TSQLRecord(Data[i]) do
  33350. if (fID<>0) and (InternalState<>State) then begin // refresh needed?
  33351. if not Refresh(fID,TSQLRecord(Data[i]),Refreshed) then
  33352. result := false; // mark error retrieving new content
  33353. end;
  33354. end;
  33355. function TSQLRestClientURI.List(const Tables: array of TSQLRecordClass;
  33356. const SQLSelect, SQLWhere: RawUTF8): TSQLTableJSON;
  33357. var Resp, SQL: RawUTF8;
  33358. U: RawUTF8;
  33359. InternalState: cardinal;
  33360. begin
  33361. result := nil;
  33362. if high(Tables)<0 then exit;
  33363. // GET Collection
  33364. SQL := Model.SQLFromSelectWhere(Tables,SQLSelect,SQLWhere);
  33365. if high(Tables)=0 then begin
  33366. // one Table -> use REST protocol (SQL as parameters)
  33367. if not IsRowID(pointer(SQLSelect)) then
  33368. // ID selected by default
  33369. U := '?select='+UrlEncode(SQLSelect) else
  33370. U := '';
  33371. if SQLWhere<>'' then begin
  33372. if U<>'' then
  33373. U := U+'&where=' else
  33374. U := U+'?where=';
  33375. U := U+UrlEncode(SQLWhere);
  33376. end;
  33377. with URI(Model.URI[TSQLRecordClass(Tables[0])]+U,'GET',@Resp) do
  33378. if Lo<>HTML_SUCCESS then
  33379. exit else
  33380. InternalState := Hi;
  33381. result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp); // get data
  33382. end else begin
  33383. // multiple tables -> send SQL statement as HTTP body
  33384. with URI(Model.Root,'GET',@Resp,nil,@SQL) do
  33385. if Lo<>HTML_SUCCESS then
  33386. exit else
  33387. InternalState := Hi;
  33388. result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp); // get data
  33389. end;
  33390. result.fInternalState := InternalState;
  33391. end;
  33392. procedure TSQLRestClientURI.SessionClose;
  33393. var tmp: RawUTF8;
  33394. begin
  33395. if (self<>nil) and (fSessionUser<>nil) and
  33396. (fSessionID<>CONST_AUTHENTICATION_SESSION_NOT_STARTED) then
  33397. try
  33398. // notify session closed to server
  33399. CallBackGet('Auth',['UserName',fSessionUser.LogonName,'Session',fSessionID],tmp);
  33400. finally
  33401. fSessionID := CONST_AUTHENTICATION_SESSION_NOT_STARTED;
  33402. fSessionIDHexa8 := '';
  33403. fSessionPrivateKey := 0;
  33404. fSessionAuthentication := nil;
  33405. fSessionServer := '';
  33406. fSessionVersion := '';
  33407. fSessionData := '';
  33408. FreeAndNil(fSessionUser);
  33409. end;
  33410. end;
  33411. function TSQLRestClientURI.SessionCreate(aAuth: TSQLRestServerAuthenticationClass;
  33412. var aUser: TSQLAuthUser; const aSessionKey: RawUTF8): boolean;
  33413. begin
  33414. result := false;
  33415. fSessionID := GetCardinal(pointer(aSessionKey));
  33416. if fSessionID=0 then
  33417. exit;
  33418. fSessionIDHexa8 := CardinalToHex(fSessionID);
  33419. fSessionPrivateKey := crc32(crc32(0,Pointer(aSessionKey),length(aSessionKey)),
  33420. pointer(aUser.PasswordHashHexa),length(aUser.PasswordHashHexa));
  33421. fSessionUser := aUser;
  33422. fSessionAuthentication := aAuth;
  33423. aUser := nil; // now owned by this instance
  33424. result := true;
  33425. end;
  33426. function TSQLRestClientURI.GetCurrentSessionUserID: TID;
  33427. begin
  33428. if fSessionUser=nil then
  33429. result := 0 else
  33430. result := fSessionUser.IDValue;
  33431. end;
  33432. constructor TSQLRestClientURI.Create(aModel: TSQLModel);
  33433. begin
  33434. inherited Create(aModel);
  33435. fSessionID := CONST_AUTHENTICATION_NOT_USED;
  33436. fFakeCallbacks := TSQLRestClientCallbacks.Create(self);
  33437. {$ifdef USELOCKERDEBUG}
  33438. fSafe := TAutoLockerDebug.Create(fLogClass,aModel.Root); // more verbose
  33439. {$else}
  33440. fSafe := TAutoLocker.Create;
  33441. {$endif}
  33442. end;
  33443. destructor TSQLRestClientURI.Destroy;
  33444. var t,i: integer;
  33445. aID: TID;
  33446. Table: TSQLRecordClass;
  33447. begin
  33448. {$ifdef MSWINDOWS}
  33449. fServiceNotificationMethodViaMessages.Wnd := 0; // disable notification
  33450. {$endif}
  33451. {$ifdef WITHLOG}
  33452. if GarbageCollectorFreeing then // may be owned by a TSynLogFamily
  33453. SetLogClass(nil);
  33454. {$endif}
  33455. fBatchCurrent.Free;
  33456. fFakeCallbacks.Free;
  33457. try
  33458. // unlock all still locked records by this client
  33459. if Model<>nil then
  33460. for t := 0 to high(Model.Locks) do begin
  33461. Table := Model.Tables[t];
  33462. with Model.Locks[t] do
  33463. for i := 0 to Count-1 do begin
  33464. aID := IDs[i];
  33465. if aID<>0 then // 0 is empty after unlock
  33466. self.UnLock(Table,aID);
  33467. end;
  33468. end;
  33469. SessionClose; // if not already notified
  33470. finally
  33471. // release memory and associated classes
  33472. if fRemoteLogClass<>nil then begin
  33473. {$ifndef LVCL}
  33474. FreeAndNil(fRemoteLogThread);
  33475. {$endif}
  33476. ServerRemoteLogStop;
  33477. end;
  33478. fSessionUser.Free;
  33479. try
  33480. inherited Destroy; // fModel.Free if owned by this TSQLRest instance
  33481. {$ifndef LVCL}
  33482. FreeAndNil(fBackgroundThread); // should be done after fServices.Free
  33483. fOnIdle := nil;
  33484. {$endif}
  33485. finally
  33486. InternalClose;
  33487. end;
  33488. end;
  33489. end;
  33490. {$ifdef SSPIAUTH}
  33491. const
  33492. SSPI_DEFINITION_USERNAME = '***SSPI***';
  33493. {$endif}
  33494. constructor TSQLRestClientURI.RegisteredClassCreateFrom(aModel: TSQLModel;
  33495. aDefinition: TSynConnectionDefinition);
  33496. begin
  33497. if fModel=nil then // if not already created with a reintroduced constructor
  33498. Create(aModel);
  33499. if fModel<>nil then
  33500. fOnIdle := fModel.OnClientIdle; // allow UI interactivity during SetUser()
  33501. if aDefinition.User<>'' then begin
  33502. {$ifdef SSPIAUTH}
  33503. if aDefinition.User=SSPI_DEFINITION_USERNAME then
  33504. SetUser('',aDefinition.PasswordPlain) else
  33505. {$endif}
  33506. SetUser(aDefinition.User,aDefinition.PasswordPlain,true);
  33507. end;
  33508. end;
  33509. procedure TSQLRestClientURI.DefinitionTo(Definition: TSynConnectionDefinition);
  33510. begin
  33511. if Definition=nil then
  33512. exit;
  33513. inherited DefinitionTo(Definition); // save Kind
  33514. if (fSessionAuthentication<>nil) and (fSessionUser<>nil) then begin
  33515. {$ifdef SSPIAUTH}
  33516. if fSessionAuthentication.InheritsFrom(TSQLRestServerAuthenticationSSPI) then
  33517. Definition.User := SSPI_DEFINITION_USERNAME else
  33518. {$endif}
  33519. Definition.User := fSessionUser.LogonName;
  33520. Definition.PasswordPlain := fSessionUser.fPasswordHashHexa;
  33521. end;
  33522. end;
  33523. procedure TSQLRestClientURI.Commit(SessionID: cardinal; RaiseException: boolean);
  33524. begin
  33525. inherited Commit(CONST_AUTHENTICATION_NOT_USED,RaiseException);
  33526. // inherited Commit = reset fTransactionActiveSession flag
  33527. URI(Model.Root,'END');
  33528. end;
  33529. procedure TSQLRestClientURI.RollBack(SessionID: cardinal);
  33530. begin
  33531. inherited RollBack(CONST_AUTHENTICATION_NOT_USED); // reset fTransactionActiveSession flag
  33532. URI(Model.Root,'ABORT');
  33533. end;
  33534. function TSQLRestClientURI.TransactionBegin(aTable: TSQLRecordClass;
  33535. SessionID: cardinal): boolean;
  33536. begin
  33537. result := inherited TransactionBegin(aTable,CONST_AUTHENTICATION_NOT_USED);
  33538. if result then
  33539. // fTransactionActiveSession flag was not already set
  33540. if aTable=nil then
  33541. result := URI(Model.Root,'BEGIN').Lo in [HTML_SUCCESS,HTML_NOCONTENT] else
  33542. result := URI(Model.URI[aTable],'BEGIN').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  33543. end;
  33544. function TSQLRestClientURI.TransactionBeginRetry(aTable: TSQLRecordClass;
  33545. Retries: integer): boolean;
  33546. begin
  33547. if Retries>50 then
  33548. Retries := 50; // avoid loop for more than 10 seconds
  33549. repeat
  33550. result := TransactionBegin(aTable);
  33551. if result then
  33552. exit;
  33553. dec(Retries);
  33554. if Retries<=0 then break;
  33555. SleepHiRes(100);
  33556. until false;
  33557. end;
  33558. const
  33559. // log up to 2 KB of JSON response, to save space
  33560. MAX_SIZE_RESPONSE_LOG = 2*1024;
  33561. function TSQLRestClientURI.CallBackGet(const aMethodName: RawUTF8;
  33562. const aNameValueParameters: array of const; out aResponse: RawUTF8;
  33563. aTable: TSQLRecordClass; aID: TID; aResponseHead: PRawUTF8): integer;
  33564. var url, header: RawUTF8;
  33565. {$ifdef WITHLOG}
  33566. Log: ISynLog; // for Enter auto-leave to work with FPC
  33567. {$endif}
  33568. begin
  33569. if self=nil then
  33570. result := HTML_UNAVAILABLE else begin
  33571. url := Model.getURICallBack(aMethodName,aTable,aID)+
  33572. UrlEncode(aNameValueParameters);
  33573. {$ifdef WITHLOG}
  33574. Log := fLogClass.Enter('CallBackGet %',[url],self);
  33575. {$endif}
  33576. result := URI(url,'GET',@aResponse,@header).Lo;
  33577. if aResponseHead<>nil then
  33578. aResponseHead^ := header;
  33579. {$ifdef WITHLOG}
  33580. if (aResponse<>'') and (sllServiceReturn in fLogFamily.Level) then
  33581. if IsHTMLContentTypeTextual(pointer(header)) then
  33582. Log.Log(sllServiceReturn,aResponse,self,MAX_SIZE_RESPONSE_LOG) else
  33583. Log.Log(sllServiceReturn,'% bytes "%"',[length(aResponse),header],self);
  33584. {$endif}
  33585. end;
  33586. end;
  33587. function TSQLRestClientURI.SetUser(const aUserName, aPassword: RawUTF8;
  33588. aHashedPassword: Boolean): boolean;
  33589. const HASH: array[boolean] of TSQLRestServerAuthenticationClientSetUserPassword =
  33590. (passClear, passHashed);
  33591. begin
  33592. if self=nil then begin
  33593. result := false;
  33594. exit;
  33595. end;
  33596. {$ifdef SSPIAUTH} // try Windows authentication with the current logged user
  33597. result := true;
  33598. if ((trim(aUserName)='') or (PosEx('\',aUserName)>0)) and
  33599. TSQLRestServerAuthenticationSSPI.ClientSetUser(self,aUserName,aPassword,passKerberosSPN) then
  33600. exit;
  33601. {$endif}
  33602. result := TSQLRestServerAuthenticationDefault.
  33603. ClientSetUser(self,aUserName,aPassword,HASH[aHashedPassword]);
  33604. end;
  33605. procedure TSQLRestClientURI.SetLastException(E: Exception; ErrorCode: integer;
  33606. Call: PSQLRestURIParams);
  33607. begin
  33608. fLastErrorCode := ErrorCode;
  33609. if E=nil then begin
  33610. fLastErrorException := nil;
  33611. if StatusCodeIsSuccess(ErrorCode) then
  33612. fLastErrorMessage := '' else
  33613. StatusCodeToErrorMsg(ErrorCode,fLastErrorMessage);
  33614. end else begin
  33615. fLastErrorException := PPointer(E)^;
  33616. fLastErrorMessage := ObjectToJSONDebug(E);
  33617. end;
  33618. if Assigned(fOnFailed) then
  33619. fOnFailed(self,E,Call);
  33620. end;
  33621. {$ifndef LVCL} // SyncObjs.TEvent not available in LVCL yet
  33622. procedure TSQLRestClientURI.OnBackgroundProcess(Sender: TSynBackgroundThreadEvent;
  33623. ProcessOpaqueParam: pointer);
  33624. var Call: ^TSQLRestURIParams absolute ProcessOpaqueParam;
  33625. begin
  33626. if Call=nil then
  33627. exit;
  33628. InternalURI(Call^);
  33629. if OnIdleBackgroundThreadActive then
  33630. if Call^.OutStatus=HTML_NOTIMPLEMENTED then begin
  33631. // InternalCheckOpen failed -> force recreate connection
  33632. InternalClose;
  33633. if OnIdleBackgroundThreadActive then
  33634. InternalURI(Call^); // try request again
  33635. end;
  33636. end;
  33637. function TSQLRestClientURI.GetOnIdleBackgroundThreadActive: boolean;
  33638. begin
  33639. result := (self<>nil) and Assigned(fOnIdle) and
  33640. fBackgroundThread.OnIdleBackgroundThreadActive;
  33641. end;
  33642. {$endif LVCL}
  33643. function TSQLRestClientURI.FakeCallbackRegister(Sender: TServiceFactoryClient;
  33644. const Method: TServiceMethod; const ParamInfo: TServiceMethodArgument;
  33645. ParamValue: Pointer): integer;
  33646. begin
  33647. raise EServiceException.CreateUTF8('% does not support interface parameters '+
  33648. 'for %.%(%: %): consider using another kind of client',
  33649. [self,Sender.fInterface.fInterfaceName,Method.URI,
  33650. ParamInfo.ParamName^,ParamInfo.ArgTypeName^]);
  33651. end;
  33652. function TSQLRestClientURI.FakeCallbackUnregister(Factory: TInterfaceFactory;
  33653. FakeCallbackID: integer; Instance: pointer): boolean;
  33654. begin
  33655. raise EServiceException.CreateUTF8(
  33656. '% does not support % callbacks: consider using another kind of client',
  33657. [self,Factory.fInterfaceTypeInfo^.Name]);
  33658. end;
  33659. function TSQLRestClientURI.URI(const url, method: RawUTF8;
  33660. Resp, Head, SendData: PRawUTF8): Int64Rec;
  33661. var Retry: integer;
  33662. aUserName, aPassword: string;
  33663. StatusMsg: RawUTF8;
  33664. Call: TSQLRestURIParams;
  33665. aRetryOnceOnTimeout, aPasswordHashed: boolean;
  33666. label DoRetry;
  33667. begin
  33668. if self=nil then begin
  33669. Int64(result) := HTML_UNAVAILABLE;
  33670. SetLastException(nil,HTML_UNAVAILABLE);
  33671. exit;
  33672. end;
  33673. aRetryOnceOnTimeout := RetryOnceOnTimeout;
  33674. fLastErrorMessage := '';
  33675. fLastErrorException := nil;
  33676. if fServerTimeStampOffset=0 then
  33677. if not ServerTimeStampSynchronize then begin
  33678. Int64(result) := HTML_UNAVAILABLE;
  33679. exit; // if /TimeStamp is not available, server is down!
  33680. end;
  33681. Call.Init;
  33682. if (Head<>nil) and (Head^<>'') then
  33683. Call.InHead := Head^;
  33684. if fSessionHttpHeader<>'' then
  33685. Call.InHead := Trim(Call.InHead+#13#10+fSessionHttpHeader);
  33686. for Retry := -1 to MaximumAuthentificationRetry do
  33687. try
  33688. DoRetry:
  33689. Call.Url := url;
  33690. if fSessionAuthentication<>nil then
  33691. fSessionAuthentication.ClientSessionSign(self,Call);
  33692. Call.Method := method;
  33693. if SendData<>nil then
  33694. Call.InBody := SendData^;
  33695. {$ifndef LVCL}
  33696. if Assigned(fOnIdle) then begin
  33697. if fBackgroundThread=nil then
  33698. fBackgroundThread := TSynBackgroundThreadEvent.Create(OnBackgroundProcess,
  33699. OnIdle,FormatUTF8('% "%" background',[self,Model.Root]));
  33700. if not fBackgroundThread.RunAndWait(@Call) then
  33701. Call.OutStatus := HTML_UNAVAILABLE;
  33702. end else
  33703. {$endif}
  33704. begin
  33705. InternalURI(Call);
  33706. if Call.OutStatus=HTML_NOTIMPLEMENTED then begin // InternalCheckOpen failed
  33707. InternalClose; // force recreate connection
  33708. InternalURI(Call); // try request again
  33709. end;
  33710. end;
  33711. result.Lo := Call.OutStatus;
  33712. result.Hi := Call.OutInternalState;
  33713. if Head<>nil then
  33714. Head^ := Call.OutHead;
  33715. if Resp<>nil then
  33716. Resp^ := Call.OutBody;
  33717. fLastErrorCode := Call.OutStatus;
  33718. if (Call.OutStatus=HTML_TIMEOUT) and aRetryOnceOnTimeout then begin
  33719. aRetryOnceOnTimeout := false;
  33720. InternalLog('% % returned "408 Request Timeout" -> RETRY',[method,url],sllError);
  33721. goto DoRetry;
  33722. end;
  33723. if not StatusCodeIsSuccess(Call.OutStatus) then begin
  33724. StatusCodeToErrorMsg(Call.OutStatus,StatusMsg);
  33725. if Call.OutBody='' then
  33726. fLastErrorMessage := StatusMsg else
  33727. fLastErrorMessage := Call.OutBody;
  33728. InternalLog('% % returned % (%) with message %',
  33729. [method,url,Call.OutStatus,StatusMsg,fLastErrorMessage],sllError);
  33730. if Assigned(fOnFailed) then
  33731. fOnFailed(self,nil,@Call);
  33732. end;
  33733. if (Call.OutStatus<>HTML_FORBIDDEN) or not Assigned(OnAuthentificationFailed) then
  33734. break;
  33735. // "403 Forbidden" in case of authentication failure -> try relog
  33736. if not OnAuthentificationFailed(Retry+2,aUserName,aPassword,aPasswordHashed) or
  33737. not SetUser(StringToUTF8(aUserName),StringToUTF8(aPassword),aPasswordHashed) then
  33738. break;
  33739. except
  33740. on E: Exception do begin
  33741. Int64(result) := HTML_NOTIMPLEMENTED; // 501
  33742. SetLastException(E,HTML_NOTIMPLEMENTED,@Call);
  33743. exit;
  33744. end;
  33745. end;
  33746. end;
  33747. function TSQLRestClientURI.CallBackGetResult(const aMethodName: RawUTF8;
  33748. const aNameValueParameters: array of const; aTable: TSQLRecordClass; aID: TID): RawUTF8;
  33749. var aResponse: RawUTF8;
  33750. begin
  33751. if CallBackGet(aMethodName,aNameValueParameters,aResponse,aTable,aID)=HTML_SUCCESS then
  33752. result := JSONDecode(aResponse) else
  33753. result := '';
  33754. end;
  33755. function TSQLRestClientURI.CallBackPut(const aMethodName,
  33756. aSentData: RawUTF8; out aResponse: RawUTF8; aTable: TSQLRecordClass;
  33757. aID: TID; aResponseHead: PRawUTF8): integer;
  33758. begin
  33759. result := CallBack(mPUT,aMethodName,aSentData,aResponse,aTable,aID,aResponseHead);
  33760. end;
  33761. function TSQLRestClientURI.CallBack(method: TSQLURIMethod;
  33762. const aMethodName,aSentData: RawUTF8; out aResponse: RawUTF8;
  33763. aTable: TSQLRecordClass; aID: TID; aResponseHead: PRawUTF8): integer;
  33764. const NAME: array[mGET..high(TSQLURIMethod)] of RawUTF8 = (
  33765. 'GET','POST','PUT','DELETE','HEAD','BEGIN','END','ABORT','LOCK','UNLOCK','STATE',
  33766. 'OPTIONS','PROPFIND','PROPPATCH','TRACE','COPY','MKCOL','MOVE','PURGE','REPORT',
  33767. 'MKACTIVITY','MKCALENDAR','CHECKOUT','MERGE','NOTIFY','PATCH','SEARCH','CONNECT');
  33768. var u: RawUTF8;
  33769. {$ifdef WITHLOG}
  33770. Log: ISynLog; // for Enter auto-leave to work with FPC
  33771. {$endif}
  33772. begin
  33773. if (self=nil) or (method<Low(NAME)) then
  33774. result := HTML_UNAVAILABLE else begin
  33775. u := Model.getURICallBack(aMethodName,aTable,aID);
  33776. {$ifdef WITHLOG}
  33777. Log := fLogClass.Enter('Callback %',[u],self);
  33778. {$endif}
  33779. result := URI(u,NAME[method],@aResponse,aResponseHead,@aSentData).Lo;
  33780. InternalLog('% result=% resplen=%',[NAME[method],result,length(aResponse)],
  33781. sllServiceReturn);
  33782. end;
  33783. end;
  33784. function TSQLRestClientURI.ServiceRegister(const aInterfaces: array of PTypeInfo;
  33785. aInstanceCreation: TServiceInstanceImplementation;
  33786. const aContractExpected: RawUTF8): boolean;
  33787. begin
  33788. result := False;
  33789. if (self=nil) or (high(aInterfaces)<0) then
  33790. exit;
  33791. result := (ServiceContainer as TServiceContainerClient).AddInterface(
  33792. aInterfaces,aInstanceCreation,aContractExpected);
  33793. end;
  33794. function TSQLRestClientURI.ServiceRegister(aInterface: PTypeInfo;
  33795. aInstanceCreation: TServiceInstanceImplementation;
  33796. const aContractExpected: RawUTF8): TServiceFactory;
  33797. begin
  33798. result := nil;
  33799. if (self=nil) or (aInterface=nil) then begin
  33800. SetLastException;
  33801. exit;
  33802. end;
  33803. with ServiceContainer as TServiceContainerClient do
  33804. try
  33805. result := AddInterface(aInterface,aInstanceCreation,aContractExpected);
  33806. except
  33807. on E: Exception do
  33808. SetLastException(E);
  33809. end;
  33810. end;
  33811. function TSQLRestClientURI.ServiceRegisterClientDriven(aInterface: PTypeInfo;
  33812. out Obj; const aContractExpected: RawUTF8): boolean;
  33813. var Factory: TServiceFactory;
  33814. begin
  33815. Factory := ServiceRegister(aInterface,sicClientDriven,aContractExpected);
  33816. if Factory<>nil then begin
  33817. result := true;
  33818. Factory.Get(Obj);
  33819. end else
  33820. result := false;
  33821. end;
  33822. function TSQLRestClientURI.ServiceDefine(const aInterfaces: array of TGUID;
  33823. aInstanceCreation: TServiceInstanceImplementation;
  33824. const aContractExpected: RawUTF8): boolean;
  33825. begin
  33826. if self<>nil then
  33827. result := ServiceRegister(TInterfaceFactory.GUID2TypeInfo(aInterfaces),
  33828. aInstanceCreation,aContractExpected) else
  33829. result := false;
  33830. end;
  33831. function TSQLRestClientURI.ServiceDefine(const aInterface: TGUID;
  33832. aInstanceCreation: TServiceInstanceImplementation;
  33833. const aContractExpected: RawUTF8): TServiceFactoryClient;
  33834. begin
  33835. result := TServiceFactoryClient(ServiceRegister(
  33836. TInterfaceFactory.GUID2TypeInfo(aInterface),aInstanceCreation,aContractExpected));
  33837. end;
  33838. function TSQLRestClientURI.ServiceDefineClientDriven(const aInterface: TGUID;
  33839. out Obj; const aContractExpected: RawUTF8): boolean;
  33840. begin
  33841. result := ServiceRegisterClientDriven(
  33842. TInterfaceFactory.GUID2TypeInfo(aInterface),Obj,aContractExpected);
  33843. end;
  33844. procedure TSQLRestClientURI.ServicePublishOwnInterfaces(OwnServer: TSQLRestServer);
  33845. begin
  33846. fServicePublishOwnInterfaces := OwnServer.ServicesPublishedInterfaces;
  33847. end;
  33848. function TSQLRestClientURI.ServiceRetrieveAssociated(const aServiceName: RawUTF8;
  33849. out URI: TSQLRestServerURIDynArray): boolean;
  33850. var json: RawUTF8;
  33851. begin
  33852. result := (CallBackGet('stat',['findservice',aServiceName],json)=HTML_SUCCESS) and
  33853. (DynArrayLoadJSON(URI,pointer(json),TypeInfo(TSQLRestServerURIDynArray))<>nil);
  33854. end;
  33855. function TSQLRestClientURI.ServiceRetrieveAssociated(const aInterface: TGUID;
  33856. out URI: TSQLRestServerURIDynArray): boolean;
  33857. var fact: TInterfaceFactory;
  33858. begin
  33859. fact := TInterfaceFactory.Get(aInterface);
  33860. if fact=nil then
  33861. result := false else
  33862. result := ServiceRetrieveAssociated(copy(fact.InterfaceName,2,maxInt),URI);
  33863. end;
  33864. function TSQLRestClientURI.EngineAdd(TableModelIndex: integer;
  33865. const SentData: RawUTF8): TID;
  33866. var P: PUTF8Char;
  33867. url, Head: RawUTF8;
  33868. begin
  33869. result := 0;
  33870. url := Model.URI[Model.Tables[TableModelIndex]];
  33871. if URI(url,'POST',nil,@Head,@SentData).Lo<>HTML_CREATED then
  33872. exit; // response must be '201 Created'
  33873. P := pointer(Head); // we need to check the headers
  33874. if P<>nil then
  33875. repeat
  33876. // find ID from 'Location: Member Entry URI' header entry
  33877. if IdemPChar(P,'LOCATION:') then begin // 'Location: root/People/11012' e.g.
  33878. inc(P,9);
  33879. while P^>#13 do inc(P); // go to end of line
  33880. P^ := #0; // make line asciiz, even if ended with #13
  33881. while P[-1] in ['0'..'9'] do dec(P); // get all number chars
  33882. if P[-1]='-' then dec(P);
  33883. result := GetInt64(P); // get numerical value at the end of the URI
  33884. exit;
  33885. end;
  33886. while not (P^ in [#0,#13]) do inc(P);
  33887. if P^=#0 then break else inc(P);
  33888. if P^=#10 then inc(P);
  33889. until false;
  33890. end;
  33891. function TSQLRestClientURI.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
  33892. var url: RawUTF8;
  33893. begin
  33894. url := Model.getURIID(Model.Tables[TableModelIndex],ID);
  33895. result := URI(url,'DELETE').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  33896. end;
  33897. function TSQLRestClientURI.EngineDeleteWhere(TableModelIndex: Integer;
  33898. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  33899. var url: RawUTF8;
  33900. begin // ModelRoot/TableName?where=WhereClause to delete members
  33901. url := Model.getURI(Model.Tables[TableModelIndex])+'?where='+UrlEncode(SQLWhere);
  33902. result := URI(url,'DELETE').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  33903. end;
  33904. function TSQLRestClientURI.EngineList(const SQL: RawUTF8;
  33905. ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
  33906. begin
  33907. if (self=nil) or (SQL='') or (ReturnedRowCount<>nil) or
  33908. (URI(Model.Root,'GET',@result,nil,@SQL).Lo<>HTML_SUCCESS) then
  33909. result := '';
  33910. end;
  33911. function TSQLRestClientURI.ClientRetrieve(TableModelIndex: integer; ID: TID;
  33912. ForUpdate: boolean; var InternalState: cardinal; var Resp: RawUTF8): boolean;
  33913. begin
  33914. if cardinal(TableModelIndex)<=cardinal(Model.fTablesMax) then
  33915. with URIGet(Model.Tables[TableModelIndex],ID,Resp,ForUpdate) do
  33916. if Lo=HTML_SUCCESS then begin
  33917. InternalState := Hi;
  33918. result := true;
  33919. end else
  33920. result := false else
  33921. result := false;
  33922. end;
  33923. function TSQLRestClientURI.EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  33924. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  33925. var url: RawUTF8;
  33926. begin
  33927. if (self=nil) or (aID<=0) or (BlobField=nil) then
  33928. result := false else begin
  33929. // URI is 'ModelRoot/TableName/TableID/BlobFieldName' with GET method
  33930. url := Model.getURICallBack(BlobField^.Name,Model.Tables[TableModelIndex],aID);
  33931. result := URI(url,'GET',@BlobData).Lo=HTML_SUCCESS;
  33932. end;
  33933. end;
  33934. function TSQLRestClientURI.EngineUpdate(TableModelIndex: integer; ID: TID;
  33935. const SentData: RawUTF8): boolean;
  33936. var url: RawUTF8;
  33937. begin
  33938. url := Model.getURIID(Model.Tables[TableModelIndex],ID);
  33939. result := URI(url,'PUT',nil,nil,@SentData).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  33940. end;
  33941. function TSQLRestClientURI.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  33942. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  33943. var url, Head: RawUTF8;
  33944. begin
  33945. Head := 'Content-Type: application/octet-stream';
  33946. if (self=nil) or (aID<=0) or (BlobField=nil) then
  33947. result := false else begin
  33948. // PUT ModelRoot/TableName/TableID/BlobFieldName
  33949. FormatUTF8('%/%/%',[Model.URI[Model.Tables[TableModelIndex]],aID,BlobField^.Name],url);
  33950. result := URI(url,'PUT',nil,@Head,@BlobData).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  33951. end;
  33952. end;
  33953. function TSQLRestClientURI.EngineUpdateField(TableModelIndex: integer;
  33954. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  33955. var url: RawUTF8;
  33956. begin
  33957. if TableModelIndex<0 then
  33958. result := false else begin
  33959. // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=..
  33960. FormatUTF8('%?setname=%&set=%&wherename=%&where=%',
  33961. [Model.URI[Model.Tables[TableModelIndex]],
  33962. SetFieldName,UrlEncode(SetValue),WhereFieldName,UrlEncode(WhereValue)],url);
  33963. result := URI(url,'PUT').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  33964. end;
  33965. end;
  33966. function TSQLRestClientURI.EngineBatchSend(Table: TSQLRecordClass; const Data: RawUTF8;
  33967. var Results: TIDDynArray; ExpectedResultsCount: integer): integer;
  33968. var Resp: RawUTF8;
  33969. R: PUTF8Char;
  33970. i: integer;
  33971. begin // TSQLRest.BatchSend() ensured that Batch contains some data
  33972. try
  33973. // URI is 'ModelRoot/Batch' or 'ModelRoot/Batch/TableName' with PUT method
  33974. result := URI(Model.getURICallBack('Batch',Table,0),'PUT',@Resp,nil,@Data).Lo;
  33975. if result<>HTML_SUCCESS then
  33976. exit;
  33977. // returned Resp shall be an array of integers: '[200,200,...]'
  33978. R := pointer(Resp);
  33979. if R<>nil then
  33980. while not (R^ in ['[',#0]) do inc(R);
  33981. result := HTML_BADREQUEST;
  33982. if (R=nil) or (R^<>'[') then
  33983. // invalid response
  33984. exit;
  33985. SetLength(Results,ExpectedResultsCount);
  33986. if IdemPChar(R,'["OK"]') then begin // to save bandwith if no adding
  33987. for i := 0 to ExpectedResultsCount-1 do
  33988. Results[i] := HTML_SUCCESS;
  33989. end else begin
  33990. inc(R); // jump first '['
  33991. for i := 0 to ExpectedResultsCount-1 do begin
  33992. Results[i] := GetJSONInt64Var(R);
  33993. while R^ in [#1..' '] do inc(R);
  33994. case R^ of
  33995. ',': inc(R);
  33996. ']': break;
  33997. else exit;
  33998. end;
  33999. end;
  34000. if R^<>']' then
  34001. exit;
  34002. end;
  34003. result := HTML_SUCCESS; // returns OK
  34004. finally
  34005. BatchAbort;
  34006. end;
  34007. end;
  34008. procedure TSQLRestClientURI.BatchAbort;
  34009. begin
  34010. if self<>nil then
  34011. FreeAndNil(fBatchCurrent);
  34012. end;
  34013. function TSQLRestClientURI.BatchAdd(Value: TSQLRecord; SendData: boolean;
  34014. ForceID: boolean=false; const CustomFields: TSQLFieldBits=[]): integer;
  34015. begin
  34016. if self=nil then
  34017. result := -1 else
  34018. result := fBatchCurrent.Add(Value,SendData,ForceID,CustomFields);
  34019. end;
  34020. function TSQLRestClientURI.BatchCount: integer;
  34021. begin
  34022. if self=nil then
  34023. result := 0 else
  34024. result := fBatchCurrent.Count;
  34025. end;
  34026. function TSQLRestClientURI.BatchDelete(ID: TID): integer;
  34027. begin
  34028. if self=nil then
  34029. result := -1 else
  34030. result := fBatchCurrent.Delete(ID);
  34031. end;
  34032. function TSQLRestClientURI.BatchDelete(Table: TSQLRecordClass; ID: TID): integer;
  34033. begin
  34034. if self=nil then
  34035. result := -1 else
  34036. result := fBatchCurrent.Delete(Table,ID);
  34037. end;
  34038. function TSQLRestClientURI.BatchStart(aTable: TSQLRecordClass;
  34039. AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions): boolean;
  34040. begin
  34041. if (self=nil) or (fBatchCurrent<>nil) then
  34042. result := false else begin
  34043. fBatchCurrent := TSQLRestBatch.Create(self,aTable,AutomaticTransactionPerRow,Options);
  34044. fBatchCurrent.fCalledWithinRest := true;
  34045. result := true;
  34046. end;
  34047. end;
  34048. function TSQLRestClientURI.BatchStartAny(AutomaticTransactionPerRow: cardinal;
  34049. Options: TSQLRestBatchOptions): boolean;
  34050. begin
  34051. result := BatchStart(nil,AutomaticTransactionPerRow,Options);
  34052. end;
  34053. function TSQLRestClientURI.BatchUpdate(Value: TSQLRecord;
  34054. const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer;
  34055. begin
  34056. if (self=nil) or (Value=nil) or (fBatchCurrent=nil) or (Value.fID<=0) or
  34057. not BeforeUpdateEvent(Value) then
  34058. result := -1 else
  34059. result := fBatchCurrent.Update(Value,CustomFields,DoNotAutoComputeFields);
  34060. end;
  34061. function TSQLRestClientURI.BatchSend(var Results: TIDDynArray): integer;
  34062. begin
  34063. if self<>nil then
  34064. try
  34065. result := BatchSend(fBatchCurrent,Results);
  34066. finally
  34067. FreeAndNil(fBatchCurrent);
  34068. end else
  34069. result := HTML_BADREQUEST;
  34070. end;
  34071. { TSQLRestServer }
  34072. const
  34073. ServerPipeNamePrefix: TFileName = '\\.\pipe\mORMot_';
  34074. var
  34075. GlobalURIRequestServer: TSQLRestServer = nil;
  34076. function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl;
  34077. function StringToPCharCopy(const s: RawUTF8): PUTF8Char;
  34078. var L: integer;
  34079. begin
  34080. L := length(s);
  34081. if L=0 then
  34082. result := nil else begin
  34083. inc(L); // copy also last #0 from s
  34084. {$ifdef MSWINDOWS}
  34085. if not USEFASTMM4ALLOC then
  34086. result := pointer(GlobalAlloc(GMEM_FIXED,L)) else
  34087. {$endif}
  34088. GetMem(result,L);
  34089. MoveFast(pointer(s)^,result^,L);
  34090. end;
  34091. end;
  34092. var call: TSQLRestURIParams;
  34093. begin
  34094. if GlobalURIRequestServer=nil then begin
  34095. Int64(result) := HTML_NOTIMPLEMENTED; // 501
  34096. exit;
  34097. end;
  34098. call.Init;
  34099. call.Url := url;
  34100. call.Method := method;
  34101. call.LowLevelConnectionID := PtrInt(GlobalURIRequestServer);
  34102. call.InHead := 'RemoteIP: 127.0.0.1';
  34103. if (Head<>nil) and (Head^<>nil) then
  34104. call.InHead := RawUTF8(Head^)+#13#10+call.InHead;
  34105. SetString(call.InBody,SendData,StrLen(SendData));
  34106. call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS;
  34107. GlobalURIRequestServer.URI(call);
  34108. result.Lo := call.OutStatus;
  34109. result.Hi := call.OutInternalState;
  34110. if Head<>nil then
  34111. Head^ := StringToPCharCopy(call.OutHead);
  34112. if Resp<>nil then
  34113. Resp^ := StringToPCharCopy(call.OutBody);
  34114. end;
  34115. procedure AdministrationExecuteGetFiles(const Folder, Mask: TFileName; const Param: RawUTF8;
  34116. var Answer: TServiceCustomAnswer);
  34117. var files: TFindFilesDynArray;
  34118. fn: TFileName;
  34119. fs: Int64;
  34120. begin
  34121. if (Param<>'*') and (PosEx(':',Param)=0) and (PosEx(PathDelim,Param)=0) then begin
  34122. fn := IncludeTrailingPathDelimiter(Folder)+UTF8ToString(Param);
  34123. fs := FileSize(fn);
  34124. if (fs>0) and (fs<256 shl 20) then begin // download up to 256 MB
  34125. Answer.Content := StringFromFile(fn);
  34126. if Answer.Content<>'' then begin
  34127. Answer.Header := BINARY_CONTENT_TYPE_HEADER+#13#10'FileName: '+Param;
  34128. exit;
  34129. end;
  34130. end;
  34131. end;
  34132. files := FindFiles(Folder,Mask,'',True,False);
  34133. Answer.Content := DynArraySaveJSON(files,TypeInfo(TFindFilesDynArray));
  34134. end;
  34135. function ReadString(Handle: cardinal): RawUTF8;
  34136. var L, Read: cardinal;
  34137. P: PUTF8Char;
  34138. begin
  34139. result := '';
  34140. if (FileRead(Handle,L,4)=4) and (L<>0) then begin
  34141. SetLength(result,L);
  34142. P := pointer(result);
  34143. repeat
  34144. Read := FileRead(Handle,P^,L);
  34145. if Read=0 then begin
  34146. SleepHiRes(100); // nothing available -> wait a little and retry
  34147. Read := FileRead(Handle,P^,L);
  34148. if Read=0 then // server may be down -> abort
  34149. raise ECommunicationException.Create('ReadString');
  34150. end;
  34151. inc(P,Read);
  34152. dec(L,Read);
  34153. until L=0; // loop until received all expected data
  34154. end;
  34155. end;
  34156. procedure WriteString(Handle: cardinal; const Text: RawUTF8);
  34157. var L: cardinal;
  34158. begin
  34159. L := length(Text);
  34160. if L=0 then
  34161. // write cardinal 0 if Text=''
  34162. FileWrite(Handle,L,4) else
  34163. // write length+content at once
  34164. {$ifdef FPC}
  34165. begin
  34166. FileWrite(Handle,L,4);
  34167. FileWrite(Handle,pointer(Text)^,L);
  34168. end;
  34169. {$else}
  34170. FileWrite(Handle,pointer(PtrInt(Text)-4)^,L+4);
  34171. {$endif}
  34172. end;
  34173. {$ifdef MSWINDOWS}
  34174. function TSQLRestServer.ExportServerNamedPipe(const ServerApplicationName: TFileName): boolean;
  34175. var PipeName: TFileName;
  34176. Pipe: THandle;
  34177. begin
  34178. result := false;
  34179. if fExportServerNamedPipeThread<>nil then
  34180. exit; // only one ExportServer() by running process
  34181. if {$ifdef UNICODE}IdemPCharW{$else}IdemPChar{$endif}(pointer(ServerApplicationName),'\\') then
  34182. PipeName := ServerApplicationName else
  34183. PipeName := ServerPipeNamePrefix+ServerApplicationName;
  34184. Pipe := FileOpen(PipeName,fmOpenReadWrite); // is this pipe existing?
  34185. if Pipe<>Invalid_Handle_Value then begin
  34186. WriteString(Pipe,''); // send integer=0 -> force server disconnect
  34187. FileClose(Pipe);
  34188. exit; // only one pipe server with this name at once
  34189. end;
  34190. fExportServerNamedPipeThread := TSQLRestServerNamedPipe.Create(self, PipeName);
  34191. NoAJAXJSON := true; // use smaller JSON size in this not HTTP use (never AJAX)
  34192. result := true; // success
  34193. end;
  34194. function TSQLRestServer.ExportServerMessage(const ServerWindowName: string): boolean;
  34195. begin
  34196. result := false;
  34197. if (self=nil) or (fServerWindow<>0) then
  34198. exit; // only one ExportServerMessage() by running process
  34199. fServerWindow := CreateInternalWindow(ServerWindowName,self);
  34200. if fServerWindow=0 then
  34201. exit; // impossible to create window -> fail
  34202. fServerWindowName := ServerWindowName;
  34203. result := true;
  34204. end;
  34205. const
  34206. MAGIC_SYN: cardinal = $A5ABA5AB;
  34207. procedure TSQLRestServer.AnswerToMessage(var Msg: TWMCopyData);
  34208. var call: TSQLRestURIParams;
  34209. P: PUTF8Char;
  34210. input: PCopyDataStruct;
  34211. Res: packed record
  34212. Magic: cardinal;
  34213. Status: cardinal;
  34214. InternalState: cardinal;
  34215. end;
  34216. Data: TCopyDataStruct;
  34217. Header, ResStr: RawUTF8;
  34218. begin
  34219. Msg.Result := HTML_NOTFOUND;
  34220. if (self=nil) or (Msg.From=0) then
  34221. exit;
  34222. input := PCopyDataStruct(Msg.CopyDataStruct);
  34223. P := input^.lpData;
  34224. if (P=nil) or (input^.cbData<=7) then
  34225. exit;
  34226. if PCardinal(P)^<>MAGIC_SYN then
  34227. exit; // invalid layout: a broadcasted WM_COPYDATA message? :(
  34228. inc(P,4);
  34229. // #1 is a field delimiter below, since Get*Item() functions return nil for #0
  34230. Msg.Result := HTML_SUCCESS; // Send something back
  34231. call.Init;
  34232. call.Url := GetNextItem(P,#1);
  34233. call.Method := GetNextItem(P,#1);
  34234. call.InHead := GetNextItem(P,#1);
  34235. call.LowLevelConnectionID := Msg.From;
  34236. Header := 'RemoteIP: 127.0.0.1';
  34237. if call.InHead='' then
  34238. call.InHead := Header else
  34239. call.InHead := call.InHead+#13#10+Header;
  34240. SetString(call.InBody,P,PtrInt(input^.cbData)-(P-input^.lpData));
  34241. call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS;
  34242. // note: it's up to URI overridden method to implement access rights
  34243. URI(call);
  34244. Res.Magic := MAGIC_SYN;
  34245. Res.Status := call.OutStatus;
  34246. Res.InternalState := call.OutInternalState;
  34247. {$ifdef FPC} // alf: to circumvent FPC issues
  34248. ResStr := '';
  34249. SetLength(ResStr,sizeof(Res)+Length(call.OutHead)+1+Length(call.OutBody));
  34250. P := pointer(ResStr);
  34251. System.Move(Pointer(@Res)^,P^,sizeof(Res));
  34252. Inc(P,sizeof(Res));
  34253. System.Move(pointer(call.OutHead)^,P^,Length(call.OutHead));
  34254. Inc(P,Length(call.OutHead));
  34255. PByte(P)^ := 1;
  34256. Inc(P);
  34257. System.Move(pointer(call.OutBody)^,P^,Length(call.OutBody));
  34258. {$else}
  34259. SetString(ResStr,PAnsiChar(@Res),sizeof(Res));
  34260. ResStr := ResStr+call.OutHead+#1+call.OutBody;
  34261. {$endif FPC}
  34262. Data.dwData := fServerWindow;
  34263. Data.cbData := length(ResStr);
  34264. Data.lpData := pointer(ResStr);
  34265. SendMessage(Msg.From,WM_COPYDATA,fServerWindow,PtrInt(@Data));
  34266. end;
  34267. function TSQLRestServer.CloseServerNamedPipe: boolean;
  34268. begin
  34269. if fExportServerNamedPipeThread<>nil then begin
  34270. fExportServerNamedPipeThread.Terminate;
  34271. SleepHiRes(200); // we have sleep(128) in TSQLRestServerNamedPipe.EngineExecute
  34272. FreeAndNil(fExportServerNamedPipeThread);
  34273. result := true;
  34274. end else
  34275. result := false;
  34276. end;
  34277. function TSQLRestServer.CloseServerMessage: boolean;
  34278. begin
  34279. result := ReleaseInternalWindow(fServerWindowName,fServerWindow);
  34280. end;
  34281. function TSQLRestServer.ExportedAsMessageOrNamedPipe: Boolean;
  34282. begin
  34283. result := (self<>nil) and
  34284. ((fExportServerNamedPipeThread<>nil) or (fServerWindow<>0));
  34285. end;
  34286. {$endif MSWINDOWS}
  34287. function TSQLRestServer.ExportServer: boolean;
  34288. begin
  34289. {$ifdef MSWINDOWS}
  34290. if (fServerWindow<>0) or (fExportServerNamedPipeThread<>nil) then
  34291. result := false else // another server was running
  34292. {$endif MSWINDOWS}
  34293. if (GlobalURIRequestServer=nil) or (GlobalURIRequestServer=self) then begin
  34294. GlobalURIRequestServer := self;
  34295. result := true;
  34296. end else
  34297. result := false;
  34298. end;
  34299. procedure TSQLRestServer.ServiceMethodRegister(aMethodName: RawUTF8;
  34300. const aEvent: TSQLRestServerCallBack; aByPassAuthentication: boolean);
  34301. begin
  34302. aMethodName := trim(aMethodName);
  34303. if aMethodName='' then
  34304. raise EServiceException.CreateUTF8('%.ServiceMethodRegister('''')',[self]);
  34305. if Model.GetTableIndex(aMethodName)>=0 then
  34306. raise EServiceException.CreateUTF8('Published method name %.% '+
  34307. 'conflicts with a Table in the Model!',[self,aMethodName]);
  34308. with PSQLRestServerMethod(fPublishedMethods.AddUniqueName(aMethodName,
  34309. 'Duplicated published method name %.%',[self,aMethodName]))^ do begin
  34310. CallBack := aEvent;
  34311. ByPassAuthentication := aByPassAuthentication;
  34312. end;
  34313. end;
  34314. procedure TSQLRestServer.ServiceMethodRegisterPublishedMethods(const aPrefix: RawUTF8;
  34315. aInstance: TObject);
  34316. var CallBack: TMethod;
  34317. {$ifdef FPC}
  34318. type
  34319. PMethodNameRec = ^TMethodNameRec;
  34320. TMethodNameRec = packed record
  34321. name: PShortString;
  34322. addr: pointer;
  34323. end;
  34324. TMethodNameTable = packed record
  34325. count: dword;
  34326. entries: packed array[0..0] of TMethodNameRec;
  34327. end;
  34328. PMethodNameTable = ^TMethodNameTable;
  34329. var methodTable: pMethodNameTable;
  34330. i: integer;
  34331. vmt: TClass;
  34332. pmr: PMethodNameRec;
  34333. begin
  34334. vmt := aInstance.ClassType;
  34335. while assigned(vmt) do begin
  34336. methodTable := PMethodNameTable((Pointer(vmt)+vmtMethodTable)^);
  34337. if Assigned(MethodTable) then begin
  34338. CallBack.Data := aInstance;
  34339. pmr := @methodTable^.entries[0];
  34340. for i := 0 to MethodTable^.count-1 do begin
  34341. CallBack.Code := pmr^.addr;
  34342. ServiceMethodRegister(aPrefix+ToUTF8(pmr^.name^),TSQLRestServerCallBack(CallBack));
  34343. inc(pmr);
  34344. end;
  34345. end;
  34346. vmt := vmt.ClassParent;
  34347. end;
  34348. end;
  34349. {$else}
  34350. var i,n: integer;
  34351. C: PtrInt;
  34352. M: PMethodInfo;
  34353. RI: PReturnInfo; // such RTTI info not available at least in Delphi 7
  34354. Param: PParamInfo;
  34355. procedure SignatureError;
  34356. begin
  34357. raise EServiceException.CreateUTF8(
  34358. 'Expected "procedure %.%(Ctxt: TSQLRestServerURIContext)" method signature',
  34359. [self,M^.Name]);
  34360. end;
  34361. begin
  34362. if aInstance=nil then
  34363. exit;
  34364. if PosEx('/',aPrefix)>0 then
  34365. raise EServiceException.CreateUTF8('%.ServiceMethodRegisterPublishedMethods'+
  34366. '("%"): prefix should not contain "/"',[self,aPrefix]);
  34367. C := PtrInt(aInstance.ClassType);
  34368. while C<>0 do begin
  34369. M := PPointer(C+vmtMethodTable)^;
  34370. if M<>nil then begin
  34371. CallBack.Data := aInstance;
  34372. n := PWord(M)^;
  34373. inc(PWord(M));
  34374. for i := 1 to n do begin
  34375. RI := M^.ReturnInfo;
  34376. if (RI<>nil) then
  34377. // $METHODINFO would also include public methods -> check signature
  34378. if (RI^.CallingConvention<>ccRegister) or (RI^.ReturnType<>nil) then
  34379. SignatureError else
  34380. case RI^.Version of
  34381. 1: ; // older Delphi revision do not have much information
  34382. 2,3: if RI^.ParamCount<>2 then // self+Ctxt
  34383. SignatureError else begin
  34384. Param := RI^.Param;
  34385. if not IdemPropName(Param^.Name,'self') then
  34386. SignatureError;
  34387. Param := Param^.Next;
  34388. if Param^.ParamType^<>TypeInfo(TSQLRestServerURIContext) then
  34389. SignatureError;
  34390. end;
  34391. else
  34392. end;
  34393. CallBack.Code := M^.Addr;
  34394. ServiceMethodRegister(aPrefix+ToUTF8(M^.Name),TSQLRestServerCallBack(CallBack));
  34395. inc(PByte(M),M^.Len);
  34396. end;
  34397. end;
  34398. C := PPtrInt(C+vmtParent)^;
  34399. if C=0 then
  34400. break else
  34401. C := PPtrInt(C)^;
  34402. end;
  34403. end;
  34404. {$endif FPC}
  34405. constructor TSQLRestServer.Create(aModel: TSQLModel; aHandleUserAuthentication: boolean);
  34406. var t: integer;
  34407. tmp: RawUTF8;
  34408. begin
  34409. if aModel=nil then
  34410. raise EORMException.CreateUTF8('%.Create(Model=nil)',[self]);
  34411. // specific server initialization
  34412. fStatLevels := SERVERDEFAULTMONITORLEVELS;
  34413. fVirtualTableDirect := true; // faster direct Static call by default
  34414. fSessions := TObjectListLocked.Create; // needed by AuthenticationRegister() below
  34415. fModel := aModel;
  34416. fSQLAuthUserClass := TSQLAuthUser;
  34417. fSQLAuthGroupClass := TSQLAuthGroup;
  34418. fSQLRecordVersionDeleteTable := TSQLRecordTableDeleted;
  34419. for t := 0 to high(Model.Tables) do
  34420. if fModel.Tables[t].RecordProps.RecordVersionField<>nil then begin
  34421. fSQLRecordVersionDeleteTable := fModel.AddTableInherited(TSQLRecordTableDeleted);
  34422. break;
  34423. end;
  34424. fSessionClass := TAuthSession;
  34425. if aHandleUserAuthentication then // default mORMot authentication schemes
  34426. AuthenticationRegister([TSQLRestServerAuthenticationDefault
  34427. {$ifdef SSPIAUTH},TSQLRestServerAuthenticationSSPI{$endif}]);
  34428. fTrackChangesHistoryTableIndexCount := length(Model.Tables);
  34429. SetLength(fTrackChangesHistory,fTrackChangesHistoryTableIndexCount);
  34430. if fTrackChangesHistoryTableIndexCount>64 then
  34431. fTrackChangesHistoryTableIndexCount := 64; // rows are identified as RecordRef
  34432. SetLength(fTrackChangesHistoryTableIndex,fTrackChangesHistoryTableIndexCount);
  34433. for t := 0 to fTrackChangesHistoryTableIndexCount-1 do
  34434. fTrackChangesHistoryTableIndex[t] := -1;
  34435. fAssociatedServices := TServicesPublishedInterfacesList.Create(0);
  34436. // abstract REST initalization
  34437. inherited Create(aModel);
  34438. fAfterCreation := true;
  34439. fStats := TSQLRestServerMonitor.Create(self);
  34440. URIPagingParameters := PAGINGPARAMETERS_YAHOO;
  34441. fSessionCounter := GetTickCount64*PtrInt(self); // pseudo-random session ID
  34442. if fSessionCounter>cardinal(maxInt) then
  34443. dec(fSessionCounter,maxInt);
  34444. // retrieve published methods
  34445. fPublishedMethods.InitSpecific(TypeInfo(TSQLRestServerMethods),
  34446. fPublishedMethod,djRawUTF8,nil,true);
  34447. ServiceMethodRegisterPublishedMethods('',self);
  34448. ServiceMethodByPassAuthentication('Auth');
  34449. ServiceMethodByPassAuthentication('TimeStamp');
  34450. tmp := 'Batch';
  34451. fPublishedMethodBatchIndex := fPublishedMethods.FindHashed(tmp);
  34452. if fPublishedMethodBatchIndex<0 then
  34453. raise EORMException.CreateUTF8('%.Create: no Batch method!',[self]);
  34454. end;
  34455. constructor TSQLRestServer.CreateWithOwnModel(const Tables: array of TSQLRecordClass;
  34456. aHandleUserAuthentication: boolean; const aRoot: RawUTF8);
  34457. var Model: TSQLModel;
  34458. begin
  34459. Model := TSQLModel.Create(Tables,aRoot);
  34460. Create(Model,aHandleUserAuthentication);
  34461. Model.Owner := self;
  34462. end;
  34463. class function TSQLRestServer.CreateInMemoryForAllVirtualTables(aModel: TSQLModel;
  34464. aHandleUserAuthentication: boolean): TSQLRestServer;
  34465. var restClass: TSQLRestClass;
  34466. fake: TSynConnectionDefinition;
  34467. begin
  34468. fake := TSynConnectionDefinition.Create;
  34469. try
  34470. fake.Kind := 'TSQLRestServerDB';
  34471. restClass := TSQLRest.ClassFrom(fake);
  34472. if (restClass=nil) or
  34473. not restClass.InheritsFrom(TSQLRestServer) then begin
  34474. // fallback if mORMotSQlite3.pas not linked
  34475. result := TSQLRestServerFullMemory.Create(aModel,aHandleUserAuthentication);
  34476. exit;
  34477. end;
  34478. fake.ServerName := ':memory:'; // avoid dependency to SynSQLite3.pas
  34479. result := TSQLRestServerClass(restClass).RegisteredClassCreateFrom(
  34480. aModel,aHandleUserAuthentication,fake);
  34481. finally
  34482. fake.Free;
  34483. end;
  34484. end;
  34485. procedure TSQLRestServer.CreateMissingTables(user_version: cardinal=0;
  34486. Options: TSQLInitializeTableOptions=[]);
  34487. begin
  34488. fCreateMissingTablesOptions := Options;
  34489. end;
  34490. procedure TSQLRestServer.InitializeTables(Options: TSQLInitializeTableOptions);
  34491. var t: integer;
  34492. begin
  34493. if (Self<>nil) and (Model<>nil) then
  34494. for t := 0 to Model.TablesMax do
  34495. if not TableHasRows(Model.Tables[t]) then
  34496. Model.Tables[t].InitializeTable(self,'',Options);
  34497. end;
  34498. constructor TSQLRestServer.RegisteredClassCreateFrom(aModel: TSQLModel;
  34499. aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition);
  34500. begin
  34501. Create(aModel,aServerHandleAuthentication);
  34502. end;
  34503. destructor TSQLRestServer.Destroy;
  34504. var i: integer;
  34505. begin
  34506. Shutdown;
  34507. if GlobalURIRequestServer=self then begin
  34508. GlobalURIRequestServer := nil;
  34509. SleepHiRes(200); // way some time any request is finished in another thread
  34510. end;
  34511. // close any running named-pipe or GDI-messages server instance
  34512. {$ifdef MSWINDOWS}
  34513. CloseServerNamedPipe;
  34514. CloseServerMessage;
  34515. {$endif}
  34516. fRecordVersionSlaveCallbacks := nil; // should be done before fServices.Free
  34517. for i := 0 to high(fStaticData) do
  34518. // free all TSQLRestStorage objects and update file if necessary
  34519. fStaticData[i].Free;
  34520. for i := 0 to high(fPublishedMethod) do
  34521. fPublishedMethod[i].Stats.Free;
  34522. FreeAndNil(fSessions);
  34523. FreeAndNil(fAssociatedServices);
  34524. ObjArrayClear(fSessionAuthentication);
  34525. inherited Destroy; // calls fServices.Free which will update fStats
  34526. FreeAndNil(fStats);
  34527. end;
  34528. procedure TSQLRestServer.Shutdown(const aStateFileName: TFileName);
  34529. {$ifdef WITHLOG}
  34530. var Log: ISynLog; // for Enter auto-leave to work with FPC
  34531. {$endif}
  34532. begin
  34533. if fSessions=nil then
  34534. exit; // avoid GPF e.g. in case of missing sqlite3-64.dll
  34535. {$ifdef WITHLOG}
  34536. Log := fLogClass.Enter('Shutdown CurrentRequestCount=% File=%',
  34537. [fStats.AddCurrentRequestCount(0),aStateFileName],self);
  34538. {$endif}
  34539. OnNotifyCallback := nil;
  34540. fSessions.Safe.Lock;
  34541. try
  34542. if fShutdownRequested then
  34543. exit; // Shutdown method already called
  34544. fShutdownRequested := true; // will be identified by TSQLRestServer.URI()
  34545. finally
  34546. fSessions.Safe.UnLock;
  34547. end;
  34548. repeat
  34549. SleepHiRes(5);
  34550. until fStats.AddCurrentRequestCount(0)=0;
  34551. if aStateFileName<>'' then
  34552. SessionsSaveToFile(aStateFileName);
  34553. end;
  34554. function TSQLRestServer.GetStaticDataServer(aClass: TSQLRecordClass): TSQLRest;
  34555. var i: cardinal;
  34556. begin
  34557. if (self<>nil) and (fStaticData<>nil) then begin
  34558. i := Model.GetTableIndexExisting(aClass);
  34559. if i<cardinal(length(fStaticData)) then
  34560. result := fStaticData[i] else
  34561. result := nil;
  34562. end else
  34563. result := nil;
  34564. end;
  34565. function TSQLRestServer.GetStaticDataServerOrVirtualTable(
  34566. aClass: TSQLRecordClass): TSQLRest;
  34567. begin
  34568. if (aClass=nil) or ((fStaticData=nil) and (fStaticVirtualTable=nil)) then
  34569. result := nil else
  34570. result := GetStaticDataServerOrVirtualTable(Model.GetTableIndexExisting(aClass));
  34571. end;
  34572. function TSQLRestServer.GetStaticDataServerOrVirtualTable(aTableIndex: integer): TSQLRest;
  34573. begin
  34574. result := nil;
  34575. if aTableIndex>=0 then begin
  34576. if cardinal(aTableIndex)<cardinal(length(fStaticData)) then
  34577. result := fStaticData[aTableIndex];
  34578. if result=nil then
  34579. if fVirtualTableDirect and (fStaticVirtualTable<>nil) then
  34580. result := fStaticVirtualTable[aTableIndex];
  34581. end;
  34582. end;
  34583. function TSQLRestServer.GetStaticDataServerOrVirtualTable(aTableIndex: integer;
  34584. out Kind: TSQLRestServerKind): TSQLRest;
  34585. begin
  34586. result := nil;
  34587. Kind := sMainEngine;
  34588. if aTableIndex>=0 then begin
  34589. if cardinal(aTableIndex)<cardinal(length(fStaticData)) then begin
  34590. result := fStaticData[aTableIndex];
  34591. if result<>nil then begin
  34592. Kind := sStaticDataTable;
  34593. exit;
  34594. end;
  34595. end;
  34596. if fVirtualTableDirect and (fStaticVirtualTable<>nil) then begin
  34597. result := fStaticVirtualTable[aTableIndex];
  34598. if result<>nil then
  34599. Kind := sVirtualTable;
  34600. end;
  34601. end;
  34602. end;
  34603. function TSQLRestServer.GetRemoteTable(TableIndex: Integer): TSQLRest;
  34604. begin
  34605. if (cardinal(TableIndex)>=cardinal(length(fStaticData))) or
  34606. (fStaticData[TableIndex]=nil) or
  34607. not fStaticData[TableIndex].InheritsFrom(TSQLRestStorageRemote) then
  34608. result := nil else
  34609. result := TSQLRestStorageRemote(fStaticData[TableIndex]).RemoteRest;
  34610. end;
  34611. function TSQLRestServer.GetVirtualTable(aClass: TSQLRecordClass): TSQLRest;
  34612. var i: integer;
  34613. begin
  34614. result := nil;
  34615. if fStaticVirtualTable<>nil then begin
  34616. i := Model.GetTableIndexExisting(aClass);
  34617. if (i>=0) and (Model.TableProps[i].Kind in IS_CUSTOM_VIRTUAL) then
  34618. result := fStaticVirtualTable[i];
  34619. end;
  34620. end;
  34621. function TSQLRestServer.IsInternalSQLite3Table(aTableIndex: integer): boolean;
  34622. begin
  34623. result := ((cardinal(aTableIndex)>=cardinal(length(fStaticData))) or
  34624. (fStaticData[aTableIndex]=nil)) and
  34625. ((cardinal(aTableIndex)>=cardinal(length(fStaticVirtualTable))) or
  34626. (fStaticVirtualTable[aTableIndex]=nil));
  34627. end;
  34628. function TSQLRestServer.StaticDataAdd(aStaticData: TSQLRestStorage): boolean;
  34629. var i,n,t: cardinal;
  34630. begin
  34631. result := false;
  34632. if (self=nil) or (aStaticData=nil) then
  34633. exit;
  34634. i := Model.GetTableIndexExisting(aStaticData.StoredClass);
  34635. n := length(fStaticData);
  34636. if (i<n) and (fStaticData[i]<>nil) and (fStaticData[i]<>aStaticData) then
  34637. exit; // TSQLRecord already registered
  34638. t := length(Model.Tables);
  34639. if n<t then
  34640. SetLength(fStaticData,t);
  34641. fStaticData[i] := aStaticData;
  34642. result := true;
  34643. end;
  34644. function TSQLRestServer.StaticDataCreate(aClass: TSQLRecordClass;
  34645. const aFileName: TFileName; aBinaryFile: boolean;
  34646. aServerClass: TSQLRestStorageInMemoryClass): TSQLRestStorage;
  34647. begin
  34648. result := TSQLRestStorage(GetStaticDataServer(aClass));
  34649. if result<>nil then begin
  34650. // class already registered -> update file name
  34651. (result as aServerClass).fFileName := aFileName;
  34652. end else begin
  34653. // class not already registered -> register now
  34654. if aServerClass=nil then
  34655. aServerClass := TSQLRestStorageInMemory; // default in-memory engine
  34656. result := aServerClass.Create(aClass,self,aFileName,aBinaryFile);
  34657. if not StaticDataAdd(result) then
  34658. raise EORMException.CreateUTF8('Error in %.StaticDataCreate(%)',[self,aClass]);
  34659. end;
  34660. end;
  34661. function TSQLRestServer.RemoteDataCreate(aClass: TSQLRecordClass;
  34662. aRemoteRest: TSQLRest): TSQLRestStorageRemote;
  34663. begin
  34664. if GetStaticDataServer(aClass)<>nil then
  34665. raise EORMException.CreateUTF8('Duplicate %.RemoteDataCreate(%)',[self,aClass]);
  34666. result := TSQLRestStorageRemote.Create(aClass,self,aRemoteRest);
  34667. if not StaticDataAdd(result) then
  34668. raise EORMException.CreateUTF8('Error in %.RemoteDataCreate(%)',[self,aClass]);
  34669. end;
  34670. procedure TSQLRestServer.FlushInternalDBCache;
  34671. begin // do nothing by default
  34672. end;
  34673. function SQLGetOrder(const SQL: RawUTF8): RawUTF8;
  34674. var P: PUTF8Char;
  34675. i: integer;
  34676. begin
  34677. i := PosI('ORDER BY ',SQL);
  34678. if i>0 then begin
  34679. inc(i,9);
  34680. while SQL[i] in [#1..' '] do inc(i); // trim left
  34681. result := copy(SQL,i,maxInt);
  34682. P := PosChar(Pointer(Result),' ');
  34683. if P=nil then
  34684. P := PosChar(Pointer(Result),';');
  34685. if P<>nil then
  34686. SetLength(result,P-pointer(Result)); // trim right
  34687. end;
  34688. if result='' then // by default, a SQLite3 query is ordered by ID
  34689. result := 'RowID';
  34690. end;
  34691. function TSQLRestServer.GetNoAJAXJSON: boolean;
  34692. begin
  34693. result := (self<>nil) and (rsoNoAJAXJSON in fOptions);
  34694. end;
  34695. procedure TSQLRestServer.SetNoAJAXJSON(const Value: boolean);
  34696. begin
  34697. if Value then
  34698. include(fOptions,rsoNoAJAXJSON) else
  34699. exclude(fOptions,rsoNoAJAXJSON);
  34700. end;
  34701. function TSQLRestServer.InternalAdaptSQL(TableIndex: integer; var SQL: RawUTF8): TSQLRest;
  34702. begin
  34703. result := nil;
  34704. if (self<>nil) and (TableIndex>=0) then begin // SQL refers to this unique table
  34705. if cardinal(TableIndex)<cardinal(length(fStaticData)) then
  34706. // no SQLite3 module available for fStaticData[] -> we need to
  34707. // retrieve manualy any static table from the SQL SELECT statement
  34708. result := fStaticData[TableIndex];
  34709. if (result=nil) and fVirtualTableDirect and (fStaticVirtualTable<>nil) then begin
  34710. result := fStaticVirtualTable[TableIndex];
  34711. // virtual table may need adaptation (e.g. RowID -> ID)
  34712. if result<>nil then
  34713. if result.InheritsFrom(TSQLRestStorage) and
  34714. not TSQLRestStorage(result).AdaptSQLForEngineList(SQL) then
  34715. // complex request will use SQlite3 virtual engine module
  34716. result := nil;
  34717. end;
  34718. end;
  34719. end;
  34720. function TSQLRestServer.InternalListRawUTF8(TableIndex: integer; const SQL: RawUTF8): RawUTF8;
  34721. var aSQL: RawUTF8;
  34722. Rest: TSQLRest;
  34723. begin
  34724. aSQL := SQL;
  34725. Rest := InternalAdaptSQL(TableIndex,aSQL);
  34726. if Rest<>nil then
  34727. // this SQL statement is handled by direct connection, faster adaptation
  34728. result := Rest.EngineList(aSQL) else
  34729. // complex TSQLVirtualTableJSON/External queries will rely on virtual table
  34730. result := MainEngineList(SQL,false,nil);
  34731. if result='[]'#$A then
  34732. result := '';
  34733. end;
  34734. const
  34735. SQLRECORDVERSION_DELETEID_SHIFT = 58;
  34736. SQLRECORDVERSION_DELETEID_RANGE = Int64(1) shl SQLRECORDVERSION_DELETEID_SHIFT;
  34737. procedure TSQLRestServer.InternalRecordVersionMaxFromExisting(RetrieveNext: PID);
  34738. var m: integer;
  34739. field: TSQLPropInfoRTTIRecordVersion;
  34740. current,max,mDeleted: Int64;
  34741. begin
  34742. fAcquireExecution[execORMWrite].Safe.Lock;
  34743. try
  34744. if fRecordVersionMax=0 then begin // check twice to avoid race condition
  34745. current := 0;
  34746. for m := 0 to Model.TablesMax do begin
  34747. field := Model.Tables[m].RecordProps.RecordVersionField;
  34748. if field<>nil then begin
  34749. if OneFieldValue(Model.Tables[m],'max('+field.Name+')','',[],[],max) then
  34750. if max>current then
  34751. current := max;
  34752. mDeleted := Int64(m) shl SQLRECORDVERSION_DELETEID_SHIFT;
  34753. if OneFieldValue(fSQLRecordVersionDeleteTable,'max(ID)','ID>? and ID<?',
  34754. [],[mDeleted,mDeleted+SQLRECORDVERSION_DELETEID_RANGE],max) then begin
  34755. max := max and pred(SQLRECORDVERSION_DELETEID_RANGE);
  34756. if max>current then
  34757. current := max;
  34758. end;
  34759. end;
  34760. end;
  34761. end else
  34762. current := fRecordVersionMax;
  34763. if RetrieveNext<>nil then begin
  34764. inc(current);
  34765. RetrieveNext^ := current;
  34766. end;
  34767. fRecordVersionMax := current;
  34768. finally
  34769. fAcquireExecution[execORMWrite].Safe.UnLock;
  34770. end;
  34771. end;
  34772. function TSQLRestServer.InternalRecordVersionComputeNext: TRecordVersion;
  34773. begin
  34774. if fRecordVersionMax=0 then
  34775. InternalRecordVersionMaxFromExisting(@result) else begin
  34776. fAcquireExecution[execORMWrite].Safe.Lock;
  34777. inc(fRecordVersionMax);
  34778. result := fRecordVersionMax;
  34779. fAcquireExecution[execORMWrite].Safe.UnLock;
  34780. end;
  34781. end;
  34782. function TSQLRestServer.RecordVersionCompute: TRecordVersion;
  34783. begin
  34784. result := InternalRecordVersionComputeNext;
  34785. if result>=SQLRECORDVERSION_DELETEID_RANGE then
  34786. raise EORMException.CreateUTF8('%.InternalRecordVersionCompute=% overflow: '+
  34787. '%.ID should be < 2^%)',[self,result,fSQLRecordVersionDeleteTable,
  34788. SQLRECORDVERSION_DELETEID_SHIFT]);
  34789. end;
  34790. function TSQLRestServer.RecordVersionCurrent: TRecordVersion;
  34791. begin
  34792. if self=nil then
  34793. result := 0 else begin
  34794. if fRecordVersionMax=0 then
  34795. InternalRecordVersionMaxFromExisting(nil);
  34796. result := fRecordVersionMax;
  34797. end;
  34798. end;
  34799. procedure TSQLRestServer.InternalRecordVersionHandle(Occasion: TSQLOccasion;
  34800. TableIndex: integer; var Decoder: TJSONObjectDecoder;
  34801. RecordVersionField: TSQLPropInfoRTTIRecordVersion);
  34802. begin
  34803. if RecordVersionField=nil then
  34804. exit; // no TRecordVersion field to track
  34805. if Decoder.FindFieldName(RecordVersionField.Name)<0 then
  34806. // only compute new monotonic TRecordVersion if not already supplied by sender
  34807. Decoder.AddFieldValue(RecordVersionField.Name,Int64ToUtf8(RecordVersionCompute),ftaNumber);
  34808. if (fServices<>nil) then
  34809. (fServices as TServiceContainerServer).RecordVersionNotifyAddUpdate(
  34810. Occasion,TableIndex,Decoder);
  34811. end;
  34812. procedure TSQLRestServer.InternalRecordVersionDelete(TableIndex: integer;
  34813. ID: TID; Batch: TSQLRestBatch);
  34814. var deleted: TSQLRecordTableDeleted;
  34815. revision: TRecordVersion;
  34816. begin
  34817. if fRecordVersionDeleteIgnore then
  34818. exit;
  34819. deleted := fSQLRecordVersionDeleteTable.Create;
  34820. try
  34821. revision := RecordVersionCompute;
  34822. deleted.IDValue := revision+Int64(TableIndex) shl SQLRECORDVERSION_DELETEID_SHIFT;
  34823. deleted.Deleted := ID;
  34824. if Batch<>nil then
  34825. Batch.Add(deleted,True,True) else
  34826. Add(deleted,True,True);
  34827. if (fServices<>nil) then
  34828. (fServices as TServiceContainerServer).RecordVersionNotifyDelete(
  34829. TableIndex,ID,Revision);
  34830. finally
  34831. deleted.Free;
  34832. end;
  34833. end;
  34834. function TSQLRestServer.RecordVersionSynchronizeSlave(Table: TSQLRecordClass;
  34835. Master: TSQLRest; ChunkRowLimit: integer; OnWrite: TOnBatchWrite): TRecordVersion;
  34836. var Writer: TSQLRestBatch;
  34837. IDs: TIDDynArray;
  34838. {$ifdef WITHLOG}
  34839. Log: ISynLog; // for Enter auto-leave to work with FPC
  34840. begin
  34841. Log := fLogClass.Enter('RecordVersionSynchronizeSlave %',[Table],self);
  34842. {$else}
  34843. begin
  34844. {$endif}
  34845. result := -1; // error
  34846. if fRecordVersionMax=0 then
  34847. InternalRecordVersionMaxFromExisting(nil);
  34848. repeat
  34849. Writer := RecordVersionSynchronizeSlaveToBatch(
  34850. Table,Master,fRecordVersionMax,ChunkRowLimit,OnWrite);
  34851. if Writer=nil then
  34852. exit; // error
  34853. if Writer.Count=0 then begin // nothing new (e.g. reached last chunk)
  34854. result := fRecordVersionMax;
  34855. Writer.Free;
  34856. break;
  34857. end else
  34858. try
  34859. fAcquireExecution[execORMWrite].Safe.Lock;
  34860. fRecordVersionDeleteIgnore := true;
  34861. if BatchSend(Writer,IDs)=HTML_SUCCESS then begin
  34862. InternalLog('%.RecordVersionSynchronize Added=% Updated=% Deleted=% on %',
  34863. [ClassType,Writer.AddCount,Writer.UpdateCount,Writer.DeleteCount,Master],sllDebug);
  34864. if ChunkRowLimit=0 then begin
  34865. result := fRecordVersionMax;
  34866. break;
  34867. end;
  34868. end else begin
  34869. InternalLog('%.RecordVersionSynchronize BatchSend() failed',[ClassType],sllError);
  34870. fRecordVersionMax := 0; // force recompute the maximum from DB
  34871. break;
  34872. end;
  34873. finally
  34874. fRecordVersionDeleteIgnore := false;
  34875. fAcquireExecution[execORMWrite].Safe.UnLock;
  34876. Writer.Free;
  34877. end;
  34878. until false;
  34879. end;
  34880. function TSQLRestServer.RecordVersionSynchronizeSlaveToBatch(Table: TSQLRecordClass;
  34881. Master: TSQLRest; var RecordVersion: TRecordVersion; MaxRowLimit: integer;
  34882. OnWrite: TOnBatchWrite): TSQLRestBatch;
  34883. var TableIndex,SourceTableIndex,UpdatedRow,DeletedRow: integer;
  34884. Props: TSQLRecordProperties;
  34885. Where: RawUTF8;
  34886. UpdatedVersion,DeletedVersion: TRecordVersion;
  34887. ListUpdated,ListDeleted: TSQLTableJSON;
  34888. Rec: TSQLRecord;
  34889. DeletedMinID: TID;
  34890. Deleted: TSQLRecordTableDeleted;
  34891. {$ifdef WITHLOG}
  34892. Log: ISynLog; // for Enter auto-leave to work with FPC
  34893. begin
  34894. Log := fLogClass.Enter('RecordVersionSynchronizeSlaveToBatch %',[Table],self);
  34895. {$else}
  34896. begin
  34897. {$endif}
  34898. result := nil;
  34899. if Master=nil then
  34900. raise EORMException.CreateUTF8('%.RecordVersionSynchronizeSlaveToBatch(Master=nil)',[self]);
  34901. TableIndex := Model.GetTableIndexExisting(Table);
  34902. SourceTableIndex := Master.Model.GetTableIndexExisting(Table); // <>TableIndex?
  34903. Props := Model.TableProps[TableIndex].Props;
  34904. if Props.RecordVersionField=nil then
  34905. raise EORMException.CreateUTF8(
  34906. '%.RecordVersionSynchronizeSlaveToBatch(%) with no TRecordVersion field',[self,Table]);
  34907. fAcquireExecution[execORMWrite].Safe.Lock;
  34908. try
  34909. Where := '%>? order by %';
  34910. if MaxRowLimit>0 then
  34911. Where := FormatUTF8('% limit %',[Where,MaxRowLimit]);
  34912. ListUpdated := Master.MultiFieldValues(Table,'*',Where,
  34913. [Props.RecordVersionField.Name,Props.RecordVersionField.Name],[RecordVersion]);
  34914. if ListUpdated=nil then
  34915. exit; // DB error
  34916. ListDeleted := nil;
  34917. try
  34918. DeletedMinID := Int64(SourceTableIndex) shl SQLRECORDVERSION_DELETEID_SHIFT;
  34919. Where := 'ID>? and ID<? order by ID';
  34920. if MaxRowLimit>0 then
  34921. Where := FormatUTF8('% limit %',[Where,MaxRowLimit]);
  34922. ListDeleted := Master.MultiFieldValues(fSQLRecordVersionDeleteTable,
  34923. 'ID,Deleted',Where,[DeletedMinID+RecordVersion,
  34924. DeletedMinID+SQLRECORDVERSION_DELETEID_RANGE]);
  34925. if ListDeleted=nil then
  34926. exit; // DB error
  34927. result := TSQLRestBatch.Create(self,nil,10000);
  34928. result.OnWrite := OnWrite;
  34929. if (ListUpdated.fRowCount=0) and (ListDeleted.fRowCount=0) then
  34930. exit; // nothing new -> returns void TSQLRestBach with Count=0
  34931. Rec := Table.Create;
  34932. Deleted := fSQLRecordVersionDeleteTable.Create;
  34933. try
  34934. Rec.FillPrepare(ListUpdated);
  34935. Deleted.FillPrepare(ListDeleted);
  34936. UpdatedRow := 1;
  34937. DeletedRow := 1;
  34938. UpdatedVersion := 0;
  34939. DeletedVersion := 0;
  34940. repeat // compute all changes in increasing version order
  34941. if UpdatedVersion=0 then
  34942. if UpdatedRow<=ListUpdated.fRowCount then begin
  34943. Rec.FillRow(UpdatedRow);
  34944. UpdatedVersion := Props.RecordVersionField.PropInfo.GetInt64Prop(Rec);
  34945. inc(UpdatedRow);
  34946. end;
  34947. if DeletedVersion=0 then
  34948. if DeletedRow<=ListDeleted.fRowCount then begin
  34949. Deleted.FillRow(DeletedRow);
  34950. DeletedVersion := Deleted.IDValue and pred(SQLRECORDVERSION_DELETEID_RANGE);
  34951. inc(DeletedRow);
  34952. end;
  34953. if (UpdatedVersion=0) and (DeletedVersion=0) then
  34954. break; // no more update available
  34955. if (UpdatedVersion>0) and
  34956. ((DeletedVersion=0) or (UpdatedVersion<DeletedVersion)) then begin
  34957. if (RecordVersion=0) or
  34958. (OneFieldValue(Table,'ID',Rec.IDValue)='') then
  34959. result.Add(Rec,true,true,Rec.fFill.TableMapFields,true) else
  34960. result.Update(Rec,[],true);
  34961. RecordVersion := UpdatedVersion;
  34962. UpdatedVersion := 0;
  34963. end else
  34964. if DeletedVersion>0 then begin
  34965. result.Delete(Table,Deleted.Deleted);
  34966. Deleted.IDValue := DeletedVersion+ // local ID follows current Model
  34967. Int64(TableIndex) shl SQLRECORDVERSION_DELETEID_SHIFT;
  34968. result.Add(Deleted,true,true,[],true);
  34969. RecordVersion := DeletedVersion;
  34970. DeletedVersion := 0;
  34971. end;
  34972. until false;
  34973. finally
  34974. Deleted.Free;
  34975. Rec.Free;
  34976. end;
  34977. finally
  34978. ListUpdated.Free;
  34979. ListDeleted.Free;
  34980. end;
  34981. finally
  34982. fAcquireExecution[execORMWrite].Safe.UnLock;
  34983. end;
  34984. end;
  34985. function TSQLRestServer.ServiceContainer: TServiceContainer;
  34986. begin
  34987. if fServices=nil then
  34988. fServices := TServiceContainerServer.Create(self);
  34989. result := fServices;
  34990. end;
  34991. function TSQLRestServer.RecordVersionSynchronizeSubscribeMaster(Table: TSQLRecordClass;
  34992. RecordVersion: TRecordVersion; const SlaveCallback: IServiceRecordVersionCallback): boolean;
  34993. begin
  34994. if self=nil then
  34995. result := false else
  34996. result := (ServiceContainer as TServiceContainerServer).
  34997. RecordVersionSynchronizeSubscribeMaster(Model.GetTableIndexExisting(Table),
  34998. RecordVersion,SlaveCallback);
  34999. end;
  35000. function TSQLRestServer.RecordVersionSynchronizeMasterStart(
  35001. ByPassAuthentication: boolean): boolean;
  35002. var factory: TServiceFactoryServer;
  35003. begin
  35004. if Services<>nil then begin
  35005. factory := Services.Info(TypeInfo(IServiceRecordVersion)) as TServiceFactoryServer;
  35006. if factory<>nil then begin
  35007. result := factory.ByPassAuthentication=ByPassAuthentication;
  35008. exit; // already registered with the same authentication parameter
  35009. end;
  35010. end;
  35011. factory := ServiceRegister(TServiceRecordVersion,[TypeInfo(IServiceRecordVersion)],sicShared);
  35012. if factory<>nil then begin
  35013. if ByPassAuthentication then
  35014. factory.ByPassAuthentication := ByPassAuthentication;
  35015. result := true;
  35016. end else
  35017. result := false;
  35018. end;
  35019. function TSQLRestServer.RecordVersionSynchronizeSlaveStart(Table: TSQLRecordClass;
  35020. MasterRemoteAccess: TSQLRestClientURI; OnNotify: TOnBatchWrite): boolean;
  35021. var current,previous: TRecordVersion;
  35022. tableIndex: integer;
  35023. tableName: RawUTF8;
  35024. service: IServiceRecordVersion;
  35025. callback: IServiceRecordVersionCallback;
  35026. retry: integer;
  35027. begin
  35028. //alfchange
  35029. callback:=nil;
  35030. result := false;
  35031. if (self=nil) or (MasterRemoteAccess=nil) then
  35032. exit;
  35033. tableIndex := Model.GetTableIndexExisting(Table);
  35034. if (fRecordVersionSlaveCallbacks<>nil) and
  35035. (fRecordVersionSlaveCallbacks[tableIndex]<>nil) then begin
  35036. InternalLog('%.RecordVersionSynchronizeSlaveStart(%): already running',[self,Table],sllWarning);
  35037. exit;
  35038. end;
  35039. tableName := Model.TableProps[tableIndex].Props.SQLTableName;
  35040. if MasterRemoteAccess.Services.Info(IServiceRecordVersion)=nil then
  35041. if not MasterRemoteAccess.ServiceRegister([TypeInfo(IServiceRecordVersion)],sicShared) then
  35042. exit;
  35043. if not MasterRemoteAccess.Services.Resolve(IServiceRecordVersion,service) then
  35044. exit;
  35045. current := 0;
  35046. retry := 0;
  35047. repeat
  35048. repeat // retrieve all pending versions (may retry up to 5 times)
  35049. previous := current;
  35050. current := RecordVersionSynchronizeSlave(Table,MasterRemoteAccess,10000,OnNotify);
  35051. if current<0 then begin
  35052. InternalLog('%.RecordVersionSynchronizeSlaveStart(%): REST failure',[self,Table],sllError);
  35053. exit;
  35054. end;
  35055. until current=previous;
  35056. // subscribe for any further modification
  35057. if callback=nil then
  35058. callback := TServiceRecordVersionCallback.Create(self,MasterRemoteAccess,Table,OnNotify);
  35059. if service.Subscribe(tableName,current,callback) then begin // push notifications
  35060. if fRecordVersionSlaveCallbacks=nil then
  35061. SetLength(fRecordVersionSlaveCallbacks,Model.TablesMax+1);
  35062. fRecordVersionSlaveCallbacks[tableIndex] := callback;
  35063. InternalLog('%.RecordVersionSynchronizeSlaveStart(%): started from revision %',
  35064. [self,Table,current],sllDebug);
  35065. result := true;
  35066. exit;
  35067. end;
  35068. // some modifications since version (i.e. last RecordVersionSynchronizeSlave)
  35069. inc(retry);
  35070. until retry=5; // avoid endless loop (most of the time, not needed)
  35071. InternalLog('%.RecordVersionSynchronizeSlaveStart(%): retry failure',[self,Table],sllError);
  35072. end;
  35073. function TSQLRestServer.RecordVersionSynchronizeSlaveStop(Table: TSQLRecordClass): boolean;
  35074. var tableIndex: integer;
  35075. begin
  35076. result := false;
  35077. if self=nil then
  35078. exit;
  35079. tableIndex := Model.GetTableIndexExisting(Table);
  35080. if (fRecordVersionSlaveCallbacks=nil) or
  35081. (fRecordVersionSlaveCallbacks[tableIndex]=nil) then begin
  35082. InternalLog('%.RecordVersionSynchronizeSlaveStop(%): not running',[self,Table],sllWarning);
  35083. exit;
  35084. end;
  35085. fRecordVersionSlaveCallbacks[tableIndex] := nil; // will notify the server
  35086. result := true;
  35087. end;
  35088. function TSQLRestServer.UnLock(Table: TSQLRecordClass; aID: TID): boolean;
  35089. begin
  35090. result := Model.UnLock(Table,aID);
  35091. end;
  35092. procedure TSQLRestServer.Commit(SessionID: cardinal; RaiseException: boolean);
  35093. var i: integer;
  35094. begin
  35095. inherited Commit(SessionID,RaiseException);
  35096. if self<>nil then
  35097. for i := 0 to high(fStaticVirtualTable) do
  35098. if fStaticVirtualTable[i]<>nil then
  35099. with TSQLRestStorageInMemory(fStaticVirtualTable[i]) do
  35100. if InheritsFrom(TSQLRestStorageInMemory) and not CommitShouldNotUpdateFile then
  35101. UpdateFile; // will do nothing if not Modified
  35102. end;
  35103. function TSQLRestServer.Delete(Table: TSQLRecordClass; ID: TID): boolean;
  35104. begin
  35105. result := inherited Delete(Table,ID); // call EngineDelete
  35106. if result then
  35107. // force relational database coherency (i.e. our FOREIGN KEY implementation)
  35108. AfterDeleteForceCoherency(Model.GetTableIndex(Table),ID);
  35109. end;
  35110. function TSQLRestServer.Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean;
  35111. var IDs: TIDDynArray;
  35112. TableIndex,i: integer;
  35113. begin
  35114. result := InternalDeleteNotifyAndGetIDs(Table,SQLWhere,IDs);
  35115. if (IDs=nil) or not result then
  35116. exit; // nothing to delete
  35117. TableIndex := Model.GetTableIndexExisting(Table);
  35118. result := EngineDeleteWhere(TableIndex,SQLWhere,IDs);
  35119. if result then
  35120. // force relational database coherency (i.e. our FOREIGN KEY implementation)
  35121. for i := 0 to high(IDs) do
  35122. AfterDeleteForceCoherency(TableIndex,IDs[i]);
  35123. end;
  35124. function TSQLRestServer.TableRowCount(Table: TSQLRecordClass): Int64;
  35125. var Rest: TSQLRest;
  35126. begin
  35127. Rest := GetStaticDataServerOrVirtualTable(Table);
  35128. if Rest<>nil then // faster direct call
  35129. result := Rest.TableRowCount(Table) else
  35130. result := inherited TableRowCount(Table);
  35131. end;
  35132. function TSQLRestServer.TableHasRows(Table: TSQLRecordClass): boolean;
  35133. var Rest: TSQLRest;
  35134. begin
  35135. Rest := GetStaticDataServerOrVirtualTable(Table);
  35136. if Rest<>nil then // faster direct call
  35137. result := Rest.TableHasRows(Table) else
  35138. result := inherited TableHasRows(Table);
  35139. end;
  35140. function TSQLRestServer.UpdateBlobFields(Value: TSQLRecord): boolean;
  35141. var Rest: TSQLRest;
  35142. begin // overridden method to update all BLOB fields at once
  35143. if (Value=nil) or (Value.fID<=0) then
  35144. result := false else begin
  35145. Rest := GetStaticDataServerOrVirtualTable(PSQLRecordClass(Value)^);
  35146. if Rest<>nil then // faster direct call
  35147. result := Rest.UpdateBlobFields(Value) else
  35148. result := inherited UpdateBlobFields(Value);
  35149. end;
  35150. end;
  35151. function TSQLRestServer.RetrieveBlobFields(Value: TSQLRecord): boolean;
  35152. var Rest: TSQLRest;
  35153. begin // overridden method to update all BLOB fields at once
  35154. if Value=nil then
  35155. result := false else begin
  35156. Rest := GetStaticDataServerOrVirtualTable(PSQLRecordClass(Value)^);
  35157. if Rest<>nil then // faster direct call
  35158. result := Rest.RetrieveBlobFields(Value) else
  35159. result := inherited RetrieveBlobFields(Value);
  35160. end;
  35161. end;
  35162. function TSQLRestServer.AfterDeleteForceCoherency(aTableIndex: integer;
  35163. aID: TID): boolean;
  35164. procedure PerformCascade(const Where: Int64; Ref: PSQLModelRecordReference);
  35165. var W: RawUTF8;
  35166. cascadeOK: boolean;
  35167. Rest: TSQLRest;
  35168. begin // set Field=0 or delete row where Field references aID
  35169. if Where=0 then
  35170. exit;
  35171. Int64ToUTF8(Where,W);
  35172. if Ref^.CascadeDelete then
  35173. cascadeOK := Delete(Model.Tables[Ref^.TableIndex],
  35174. Ref^.FieldType.Name+'=:('+W+'):') else begin
  35175. Rest := GetStaticDataServerOrVirtualTable(Ref^.TableIndex);
  35176. if Rest<>nil then // fast direct call
  35177. cascadeOK := Rest.EngineUpdateField(Ref^.TableIndex,
  35178. Ref^.FieldType.Name,'0',Ref^.FieldType.Name,W) else
  35179. cascadeOK := MainEngineUpdateField(Ref^.TableIndex,
  35180. Ref^.FieldType.Name,'0',Ref^.FieldType.Name,W);
  35181. end;
  35182. if not cascadeOK then
  35183. InternalLog('%.AfterDeleteForceCoherency() failed to handle field %.%',
  35184. [ClassType,Model.Tables[Ref^.TableIndex],Ref^.FieldType.Name],sllWarning);
  35185. end;
  35186. var i: integer;
  35187. Ref: PSQLModelRecordReference;
  35188. begin
  35189. Ref := @Model.fRecordReferences[0];
  35190. if Ref<>nil then begin
  35191. for i := 1 to length(Model.fRecordReferences) do begin
  35192. if Ref^.FieldTableIndex=-2 then // lazy initialization
  35193. Ref^.FieldTableIndex := Model.GetTableIndexSafe(Ref^.FieldTable,false);
  35194. case Ref^.FieldType.SQLFieldType of
  35195. sftRecord: // TRecordReference published field
  35196. PerformCascade(RecordReference(aTableIndex,aID),Ref);
  35197. sftID: // TSQLRecord published field
  35198. if Ref^.FieldTableIndex=aTableIndex then
  35199. PerformCascade(aID,Ref);
  35200. sftTID: // TTableID = type TID published field
  35201. if Ref^.FieldTableIndex=aTableIndex then
  35202. PerformCascade(aID,Ref);
  35203. end;
  35204. inc(Ref);
  35205. end;
  35206. end;
  35207. result := true; // success even if no match found, or some cascade warnings
  35208. end;
  35209. function TSQLRestServer.CreateSQLMultiIndex(Table: TSQLRecordClass;
  35210. const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8=''): boolean;
  35211. var SQL: RawUTF8;
  35212. i, TableIndex: integer;
  35213. Props: TSQLRecordProperties;
  35214. Rest: TSQLRest;
  35215. begin
  35216. result := false;
  35217. if high(FieldNames)<0 then
  35218. exit; // avoid endless loop for TSQLRestStorage with no overridden method
  35219. TableIndex := Model.GetTableIndexExisting(Table);
  35220. Rest := nil;
  35221. if TableIndex>=0 then begin // bypass fVirtualTableDirect
  35222. if cardinal(TableIndex)<cardinal(length(fStaticData)) then
  35223. Rest := fStaticData[TableIndex];
  35224. if (Rest=nil) and (fStaticVirtualTable<>nil) then
  35225. Rest := fStaticVirtualTable[TableIndex];
  35226. end;
  35227. if Rest<>nil then begin
  35228. if Rest.InheritsFrom(TSQLRestStorage) then
  35229. // will try to create an index on the static table (e.g. for external DB)
  35230. result := TSQLRestStorage(Rest).
  35231. CreateSQLMultiIndex(Table,FieldNames,Unique,IndexName);
  35232. exit;
  35233. end;
  35234. if (high(FieldNames)=0) and IsRowID(pointer(FieldNames[0])) then begin
  35235. result := true; // SQLite3 has always its ID/RowID primary key indexed
  35236. exit;
  35237. end;
  35238. Props := Model.TableProps[TableIndex].Props;
  35239. for i := 0 to high(FieldNames) do
  35240. if not IsRowID(pointer(FieldNames[i])) then
  35241. if (Props.Fields.IndexByName(FieldNames[i])<0) then
  35242. exit; // wrong field name
  35243. if Unique then
  35244. SQL := 'UNIQUE ' else
  35245. SQL := '';
  35246. if IndexName='' then begin
  35247. IndexName := RawUTF8ArrayToCSV(FieldNames,'');
  35248. if length(IndexName)+length(Props.SQLTableName)>64 then
  35249. // avoid reaching potential identifier name size limit
  35250. IndexName := crc32cUTF8ToHex(Props.SQLTableName)+
  35251. crc32cUTF8ToHex(IndexName);
  35252. end;
  35253. SQL := FormatUTF8('CREATE %INDEX IF NOT EXISTS Index%% ON %(%);',
  35254. [SQL,Props.SQLTableName,IndexName,Props.SQLTableName,RawUTF8ArrayToCSV(FieldNames,',')]);
  35255. result := EngineExecute(SQL);
  35256. end;
  35257. function TSQLRestServer.CreateSQLIndex(Table: TSQLRecordClass; const FieldName: RawUTF8;
  35258. Unique: boolean; const IndexName: RawUTF8=''): boolean;
  35259. begin
  35260. result := CreateSQLMultiIndex(Table,[FieldName],Unique,IndexName);
  35261. end;
  35262. function TSQLRestServer.CreateSQLIndex(Table: TSQLRecordClass;
  35263. const FieldNames: array of RawUTF8; Unique: boolean): boolean;
  35264. var i: integer;
  35265. begin
  35266. result := true;
  35267. for i := 0 to high(FieldNames) do
  35268. if not CreateSQLMultiIndex(Table,[FieldNames[i]],Unique) then
  35269. result := false;
  35270. end;
  35271. function TSQLRestServer.GetAuthenticationSchemesCount: integer;
  35272. begin
  35273. result := length(fSessionAuthentication);
  35274. end;
  35275. function TSQLRestServer.GetCurrentSessionUserID: TID;
  35276. begin
  35277. with PServiceRunningContext(@ServiceContext)^ do
  35278. if (Request<>nil) and (Request.Session>CONST_AUTHENTICATION_NOT_USED) then
  35279. result := Request.SessionUser else
  35280. result := 0;
  35281. end;
  35282. function TSQLRestServer.AuthenticationRegister(
  35283. aMethod: TSQLRestServerAuthenticationClass): TSQLRestServerAuthentication;
  35284. var i: integer;
  35285. begin
  35286. result := nil;
  35287. if self=nil then
  35288. exit;
  35289. fSessions.Safe.Lock;
  35290. try
  35291. for i := 0 to high(fSessionAuthentication) do
  35292. if fSessionAuthentication[i].ClassType=aMethod then begin
  35293. result := fSessionAuthentication[i];
  35294. exit; // method already there
  35295. end;
  35296. // create and initialize new authentication instance
  35297. result := aMethod.Create(self);
  35298. ObjArrayAdd(fSessionAuthentication,result); // will be owned by fSessionAuthentications
  35299. fHandleAuthentication := true;
  35300. // we need both AuthUser+AuthGroup tables for authentication -> create now
  35301. fSQLAuthGroupClass := Model.AddTableInherited(TSQLAuthGroup);
  35302. fSQLAuthUserClass := Model.AddTableInherited(TSQLAuthUser);
  35303. if fAfterCreation and
  35304. ((not TableHasRows(fSQLAuthUserClass)) or
  35305. (not TableHasRows(fSQLAuthGroupClass))) then
  35306. CreateMissingTables(0,fCreateMissingTablesOptions);
  35307. finally
  35308. fSessions.Safe.UnLock;
  35309. end;
  35310. end;
  35311. procedure TSQLRestServer.AuthenticationRegister(
  35312. const aMethods: array of TSQLRestServerAuthenticationClass);
  35313. var i: integer;
  35314. begin
  35315. for i := 0 to high(aMethods) do
  35316. AuthenticationRegister(aMethods[i]);
  35317. end;
  35318. procedure TSQLRestServer.AuthenticationUnregister(aMethod: TSQLRestServerAuthenticationClass);
  35319. var i: integer;
  35320. begin
  35321. if (self=nil) or (fSessionAuthentication=nil) then
  35322. exit;
  35323. fSessions.Safe.Lock;
  35324. try
  35325. for i := 0 to high(fSessionAuthentication) do
  35326. if fSessionAuthentication[i].ClassType=aMethod then begin
  35327. ObjArrayDelete(fSessionAuthentication,i);
  35328. fHandleAuthentication := (fSessionAuthentication<>nil);
  35329. break;
  35330. end;
  35331. finally
  35332. fSessions.Safe.UnLock;
  35333. end;
  35334. end;
  35335. procedure TSQLRestServer.AuthenticationUnregister(
  35336. const aMethods: array of TSQLRestServerAuthenticationClass);
  35337. var i: integer;
  35338. begin
  35339. for i := 0 to high(aMethods) do
  35340. AuthenticationUnregister(aMethods[i]);
  35341. end;
  35342. procedure TSQLRestServer.AuthenticationUnregisterAll;
  35343. begin
  35344. if (self=nil) or (fSessionAuthentication=nil) then
  35345. exit;
  35346. fSessions.Safe.Lock;
  35347. ObjArrayClear(fSessionAuthentication);
  35348. fSessions.Safe.UnLock;
  35349. end;
  35350. procedure TSQLRestServer.ServiceMethodByPassAuthentication(const aMethodName: RawUTF8);
  35351. var i: Integer;
  35352. begin
  35353. if self=nil then
  35354. exit;
  35355. if aMethodName='' then
  35356. for i := 0 to fPublishedMethods.Count-1 do
  35357. fPublishedMethod[i].ByPassAuthentication := true else begin
  35358. i := fPublishedMethods.FindHashed(aMethodName);
  35359. if i>=0 then
  35360. fPublishedMethod[i].ByPassAuthentication := true;
  35361. end;
  35362. end;
  35363. function TSQLRestServer.GetServiceMethodStat(const aMethod: RawUTF8): TSynMonitorInputOutput;
  35364. var i: Integer;
  35365. begin
  35366. if self=nil then
  35367. i := -1 else
  35368. i := fPublishedMethods.FindHashed(aMethod);
  35369. if i>=0 then
  35370. result := fPublishedMethod[i].Stats else
  35371. result := nil;
  35372. end;
  35373. procedure TSQLRestServer.SetPublicURI(const Address,Port: RawUTF8);
  35374. begin
  35375. fPublicURI.Address := Address;
  35376. fPublicURI.Port := Port;
  35377. fPublicURI.Root := Model.Root;
  35378. end;
  35379. const // text definition registered in unit's initialization block below
  35380. _TSQLRestServerURI = 'Address,Port,Root RawUTF8';
  35381. _TServicesPublishedInterfaces =
  35382. 'PublicURI{Address,Port,Root RawUTF8} Names array of RawUTF8';
  35383. function TSQLRestServer.ServicesPublishedInterfaces: RawUTF8;
  35384. var nfo: TServicesPublishedInterfaces;
  35385. begin
  35386. if (self=nil) or (Services=nil) then
  35387. result := '' else begin
  35388. nfo.PublicURI := fPublicURI;
  35389. Services.SetInterfaceNames(nfo.Names);
  35390. result := RecordSaveJSON(nfo,TypeInfo(TServicesPublishedInterfaces));
  35391. end;
  35392. end;
  35393. { Low-level background execution functions }
  35394. type
  35395. TInterfacedObjectHooked = class(TInterfacedObject)
  35396. public
  35397. procedure InternalRelease;
  35398. end;
  35399. TBackgroundLauncherAction = (
  35400. doCallMethod, doInstanceRelease, doThreadMethod);
  35401. PBackgroundLauncher = ^TBackgroundLauncher;
  35402. TBackgroundLauncher = record
  35403. Context: PServiceRunningContext;
  35404. case Action: TBackgroundLauncherAction of
  35405. doCallMethod:
  35406. (CallMethodArgs: pointer); // PCallMethodArgs
  35407. doInstanceRelease:
  35408. (Instance: TInterfacedObjectHooked);
  35409. doThreadMethod:
  35410. (ThreadMethod: TThreadMethod)
  35411. end;
  35412. procedure TInterfacedObjectHooked.InternalRelease;
  35413. begin
  35414. if self<>nil then
  35415. IInterface(self)._Release; // call the release interface
  35416. end;
  35417. procedure BackgroundExecuteProc(Call: pointer); forward;
  35418. {$ifdef DELPHI6OROLDER} {$ifndef LVCL}
  35419. type TThreadHook = class(TThread);
  35420. {$endif} {$endif}
  35421. procedure BackGroundExecute(var synch: TBackgroundLauncher;
  35422. backgroundThread: TSynBackgroundThreadMethod);
  35423. var event: TThreadMethod;
  35424. {$ifdef DELPHI6OROLDER} {$ifndef LVCL}
  35425. tempThread: TThread;
  35426. {$endif} {$endif}
  35427. begin
  35428. synch.Context := @ServiceContext;
  35429. TMethod(event).Code := @BackgroundExecuteProc;
  35430. TMethod(event).Data := @synch;
  35431. if backgroundThread=nil then
  35432. if GetCurrentThreadID=MainThreadID then
  35433. event else
  35434. {$ifdef LVCL}
  35435. raise EServiceException.Create('BackGroundExecute(thread=nil)')
  35436. {$else}
  35437. {$ifdef DELPHI6OROLDER}
  35438. if synch.Context^.RunningThread=nil then begin
  35439. // circumvent Delphi 6 limitation by using a temporary TThread
  35440. tempThread := TThread.Create(true);
  35441. try
  35442. TThreadHook(tempThread).Synchronize(event)
  35443. finally
  35444. tempThread.Free; // slightly slower, but working
  35445. end;
  35446. end else
  35447. TThreadHook(synch.Context^.RunningThread).Synchronize(event)
  35448. {$else}
  35449. TThread.Synchronize(synch.Context^.RunningThread,event)
  35450. {$endif DELPHI6OROLDER}
  35451. {$endif LVCL} else
  35452. backgroundThread.RunAndWait(event);
  35453. end;
  35454. procedure BackgroundExecuteCallMethod(args: pointer;
  35455. backgroundThread: TSynBackgroundThreadMethod);
  35456. var synch: TBackgroundLauncher;
  35457. begin
  35458. synch.Action := doCallMethod;
  35459. synch.CallMethodArgs := args;
  35460. BackGroundExecute(synch,backgroundThread);
  35461. end;
  35462. procedure BackgroundExecuteInstanceRelease(instance: TObject;
  35463. backgroundThread: TSynBackgroundThreadMethod);
  35464. var synch: TBackgroundLauncher;
  35465. begin
  35466. synch.Action := doInstanceRelease;
  35467. if not instance.InheritsFrom(TInterfacedObject) then
  35468. raise EServiceException.CreateUTF8('BackgroundExecuteInstanceRelease(%)',[instance]);
  35469. synch.Instance := TInterfacedObjectHooked(instance);
  35470. BackGroundExecute(synch,backgroundThread);
  35471. end;
  35472. procedure BackgroundExecuteThreadMethod(const method: TThreadMethod;
  35473. backgroundThread: TSynBackgroundThreadMethod);
  35474. var synch: TBackgroundLauncher;
  35475. begin
  35476. synch.Action := doThreadMethod;
  35477. synch.ThreadMethod := method;
  35478. BackGroundExecute(synch,backgroundThread);
  35479. end;
  35480. { TSQLRestServerURIContext }
  35481. constructor TSQLRestServerURIContext.Create(aServer: TSQLRestServer;
  35482. const aCall: TSQLRestURIParams);
  35483. begin
  35484. Server := aServer;
  35485. Call := @aCall;
  35486. Method := StringToMethod(aCall.method);;
  35487. fThreadServer := @ServiceContext;
  35488. fThreadServer^.Request := self;
  35489. end;
  35490. destructor TSQLRestServerURIContext.Destroy;
  35491. begin
  35492. fThreadServer^.Request := nil;
  35493. inherited Destroy;
  35494. end;
  35495. procedure TSQLRestServerURIContext.InternalSetTableFromTableName(TableName: PUTF8Char);
  35496. begin
  35497. TableEngine := Server;
  35498. InternalSetTableFromTableIndex(Server.Model.GetTableIndex(TableName));
  35499. if TableIndex<0 then
  35500. exit;
  35501. Static := Server.GetStaticDataServerOrVirtualTable(TableIndex,StaticKind);
  35502. if Static<>nil then
  35503. TableEngine := Static;
  35504. end;
  35505. procedure TSQLRestServerURIContext.InternalSetTableFromTableIndex(Index: integer);
  35506. begin
  35507. TableIndex := Index;
  35508. if TableIndex>=0 then
  35509. with Server.Model do begin
  35510. self.Table := Tables[TableIndex];
  35511. self.TableRecordProps := TableProps[TableIndex];
  35512. end;
  35513. end;
  35514. function TSQLRestServerURIContext.URIDecodeREST: boolean;
  35515. var i,j,slash: integer;
  35516. Par: PUTF8Char;
  35517. begin // expects 'ModelRoot[/TableName[/TableID][/URIBlobFieldName]][?param=...]' format
  35518. // check root URI and Parameters
  35519. i := 0;
  35520. if (Call^.url<>'') and (Call^.url[1]='/') then
  35521. inc(i); // URL may be '/path'
  35522. j := length(Server.Model.Root);
  35523. if (i+j>length(Call^.Url)) or (not(Call^.Url[i+j+1] in [#0,'/','?'])) or
  35524. (StrCompIL(pointer(PtrInt(Call^.url)+i),pointer(Server.Model.Root),j,0)<>0) then begin
  35525. result := False;
  35526. exit; // bad ModelRoot -> caller can try another TSQLRestServer
  35527. end;
  35528. ParametersPos := PosEx(RawUTF8('?'),Call^.url,1);
  35529. if ParametersPos>0 then // '?select=...&where=...' or '?where=...'
  35530. Parameters := @Call^.url[ParametersPos+1];
  35531. if Method=mPost then begin
  35532. fInputPostContentType := Call^.InBodyType(false);
  35533. if (Parameters=nil) and
  35534. IdemPChar(pointer(fInputPostContentType),'APPLICATION/X-WWW-FORM-URLENCODED') then
  35535. Parameters := pointer(Call^.InBody);
  35536. end;
  35537. // compute URI without any root nor parameter
  35538. inc(i,j+2);
  35539. if ParametersPos=0 then
  35540. URI := copy(Call^.url,i,maxInt) else
  35541. URI := copy(Call^.url,i,ParametersPos-i);
  35542. // compute Table, TableID and URIBlobFieldName
  35543. slash := PosEx(RawUTF8('/'),URI);
  35544. if slash>0 then begin
  35545. URI[slash] := #0;
  35546. Par := pointer(URI);
  35547. InternalSetTableFromTableName(Par);
  35548. inc(Par,slash);
  35549. if (Table<>nil) and (Par^ in ['0'..'9']) then
  35550. // "ModelRoot/TableName/TableID/URIBlobFieldName"
  35551. TableID := GetNextItemInt64(Par,'/') else
  35552. TableID := -1; // URI like "ModelRoot/TableName/MethodName"
  35553. URIBlobFieldName := Par;
  35554. if Table<>nil then begin
  35555. j := PosEx('/',URIBlobFieldName);
  35556. if j>0 then begin // handle "ModelRoot/TableName/URIBlobFieldName/ID"
  35557. TableID := GetCardinalDef(pointer(PtrInt(URIBlobFieldName)+j),cardinal(-1));
  35558. SetLength(URIBlobFieldName,j-1);
  35559. end;
  35560. end;
  35561. SetLength(URI,slash-1);
  35562. end else
  35563. InternalSetTableFromTableName(pointer(URI)); // "ModelRoot/TableName"
  35564. // compute URISessionSignaturePos and URIWithoutSignature
  35565. if ParametersPos>0 then
  35566. if IdemPChar(Parameters,'SESSION_SIGNATURE=') then
  35567. URISessionSignaturePos := ParametersPos else
  35568. URISessionSignaturePos := PosEx('&session_signature=',Call^.url,ParametersPos+1);
  35569. if URISessionSignaturePos=0 then
  35570. URIWithoutSignature := Call^.Url else
  35571. URIWithoutSignature := Copy(Call^.Url,1,URISessionSignaturePos-1);
  35572. result := True;
  35573. end;
  35574. procedure TSQLRestServerURIContext.URIDecodeSOAByMethod;
  35575. begin
  35576. if Table=nil then
  35577. // check URI as 'ModelRoot/MethodName'
  35578. MethodIndex := Server.fPublishedMethods.FindHashed(URI) else
  35579. if URIBlobFieldName<>'' then
  35580. // check URI as 'ModelRoot/TableName[/TableID]/MethodName'
  35581. MethodIndex := Server.fPublishedMethods.FindHashed(URIBlobFieldName) else
  35582. MethodIndex := -1;
  35583. end;
  35584. var // as set by TSQLRestServer.AdministrationExecute()
  35585. BYPASS_ACCESS_RIGHTS: TSQLAccessRights;
  35586. function TSQLRestServerURIContext.Authenticate: boolean;
  35587. var aSession: TAuthSession;
  35588. i: integer;
  35589. begin
  35590. if Server.HandleAuthentication and not IsRemoteAdministrationExecute then begin
  35591. Session := CONST_AUTHENTICATION_SESSION_NOT_STARTED;
  35592. result := false;
  35593. Server.fSessions.Safe.Lock;
  35594. try
  35595. if Server.fSessionAuthentication<>nil then
  35596. for i := 0 to length(Server.fSessionAuthentication)-1 do begin
  35597. aSession := Server.fSessionAuthentication[i].RetrieveSession(self);
  35598. if aSession<>nil then begin
  35599. {$ifdef WITHLOG}
  35600. Log.Log(sllUserAuth,'%/% %',[aSession.User.LogonName,aSession.ID,
  35601. aSession.RemoteIP],self);
  35602. {$endif}
  35603. fSessionAccessRights := aSession.fAccessRights; // local copy
  35604. Call^.RestAccessRights := @fSessionAccessRights;
  35605. Session := aSession.IDCardinal;
  35606. result := true;
  35607. exit;
  35608. end;
  35609. end;
  35610. finally
  35611. Server.fSessions.Safe.UnLock;
  35612. end;
  35613. // if we reached here, no session was found
  35614. if Service<>nil then
  35615. // you can allow a service to be called directly
  35616. result := Service.ByPassAuthentication else
  35617. if MethodIndex>=0 then
  35618. // /auth + /timestamp are e.g. allowed methods without signature
  35619. result := Server.fPublishedMethod[MethodIndex].ByPassAuthentication else
  35620. if (Table<>nil) and (Method in Server.fBypassORMAuthentication) then
  35621. // allow by-pass for a set of HTTP verbs (e.g. mGET)
  35622. result := true;
  35623. end else begin // default unique session if authentication is not enabled
  35624. Session := CONST_AUTHENTICATION_NOT_USED;
  35625. result := true;
  35626. end;
  35627. end;
  35628. procedure TSQLRestServerURIContext.AuthenticationFailed(
  35629. Reason: TNotifyAuthenticationFailedReason);
  35630. begin
  35631. {$ifdef WITHLOG}
  35632. Log.Log(sllUserAuth,'AuthenticationFailed(%) for % (session=%)',[GetEnumName(
  35633. TypeInfo(TNotifyAuthenticationFailedReason),ord(Reason))^,Call^.Url,Session],self);
  35634. {$endif}
  35635. // 401 Unauthorized response MUST include a WWW-Authenticate header,
  35636. // which is not what we used, so here we won't send 401 error code but 403
  35637. Call.OutStatus := HTML_FORBIDDEN;
  35638. // call the notification event
  35639. if Assigned(Server.OnAuthenticationFailed) then
  35640. Server.OnAuthenticationFailed(Server,Reason,nil,self);
  35641. end;
  35642. destructor TSQLRestAcquireExecution.Destroy;
  35643. begin
  35644. inherited Destroy;
  35645. Thread.Free;
  35646. end;
  35647. procedure TSQLRestServerURIContext.ExecuteCommand;
  35648. procedure TimeOut;
  35649. begin
  35650. {$ifdef WITHLOG}
  35651. Log.Log(sllServer,'TimeOut %.Execute(%) after % ms',[self,ToText(Command)^,
  35652. Server.fAcquireExecution[Command].LockedTimeOut],self);
  35653. {$endif}
  35654. if Call<>nil then
  35655. Call^.OutStatus := HTML_TIMEOUT; // 408 Request Time-out
  35656. end;
  35657. var Method: TThreadMethod;
  35658. Start64: Int64;
  35659. begin
  35660. with Server.fAcquireExecution[Command] do begin
  35661. case Command of
  35662. execSOAByMethod:
  35663. Method := ExecuteSOAByMethod;
  35664. execSOAByInterface:
  35665. Method := ExecuteSOAByInterface;
  35666. execORMGet:
  35667. Method := ExecuteORMGet;
  35668. execORMWrite: begin // special behavior to handle transactions at writing
  35669. Method := ExecuteORMWrite;
  35670. Start64 := GetTickCount64;
  35671. repeat
  35672. if Safe.TryLock then
  35673. try
  35674. if (Server.fTransactionActiveSession=0) or // avoid transaction mixups
  35675. (Server.fTransactionActiveSession=Session) then begin
  35676. if Mode=amLocked then begin
  35677. ExecuteORMWrite; // process within the obtained write mutex
  35678. exit;
  35679. end;
  35680. break; // will handle Mode<>amLocked below
  35681. end;
  35682. finally
  35683. Safe.UnLock;
  35684. end;
  35685. if (LockedTimeOut<>0) and (GetTickCount64>Start64+LockedTimeOut) then begin
  35686. TimeOut; // wait up to 2 second by default
  35687. exit;
  35688. end;
  35689. SleepHiRes(1); // retry every 1 ms
  35690. until false;
  35691. end;
  35692. else raise EORMException.CreateUTF8('Unexpected Command=% in %.Execute',
  35693. [ord(Command),self]);
  35694. end;
  35695. if Mode=amBackgroundORMSharedThread then
  35696. if (Command=execORMWrite) and
  35697. (Server.fAcquireExecution[execORMGet].Mode=amBackgroundORMSharedThread) then
  35698. Command := execORMGet; // for share same thread for ORM read/write
  35699. end;
  35700. with Server.fAcquireExecution[Command] do
  35701. case Mode of
  35702. amUnlocked:
  35703. Method;
  35704. amLocked:
  35705. if LockedTimeOut=0 then begin
  35706. Safe.Lock;
  35707. try
  35708. Method;
  35709. finally
  35710. Safe.UnLock;
  35711. end;
  35712. end else begin
  35713. Start64 := GetTickCount64;
  35714. repeat
  35715. if Safe.TryLock then
  35716. try
  35717. Method;
  35718. finally
  35719. Safe.UnLock;
  35720. end;
  35721. if GetTickCount64>Start64+LockedTimeOut then
  35722. break; // wait up to 2 second by default
  35723. SleepHiRes(1); // retry every 1 ms
  35724. until false;
  35725. TimeOut;
  35726. end;
  35727. {$ifndef LVCL}
  35728. amMainThread:
  35729. BackgroundExecuteThreadMethod(Method,nil);
  35730. {$endif}
  35731. amBackgroundThread,amBackgroundORMSharedThread: begin
  35732. if Thread=nil then
  35733. Thread := Server.NewBackgroundThreadMethod('% "%" %',
  35734. [self,Server.Model.Root,ToText(Command)^]);
  35735. BackgroundExecuteThreadMethod(Method,Thread);
  35736. end;
  35737. end;
  35738. end;
  35739. procedure TSQLRestServerURIContext.ConfigurationRestMethod(SettingsStorage: TObject);
  35740. var value: TDocVariantData;
  35741. valid: boolean;
  35742. config: variant;
  35743. begin
  35744. URIBlobFieldName := StringReplaceChars(URIBlobFieldName,'/','.');
  35745. if InputExists['value'] then begin
  35746. if URIBlobFieldName='' then
  35747. exit;
  35748. value.InitObjectFromPath(URIBlobFieldName,Input['value']);
  35749. JsonToObject(SettingsStorage,pointer(value.ToJSON),valid,nil,JSONTOOBJECT_TOLERANTOPTIONS);
  35750. if not valid then begin
  35751. Error('Invalid input [%] - expected %',[variant(value),
  35752. ClassFieldNamesAllPropsAsText(SettingsStorage.ClassType,true)]);
  35753. exit;
  35754. end;
  35755. end;
  35756. ObjectToVariant(SettingsStorage,config,[woDontStoreDefault]);
  35757. if URIBlobFieldName<>'' then
  35758. config := TDocVariantData(config).GetValueByPath(URIBlobFieldName);
  35759. ReturnsJson(config,HTML_SUCCESS,true,twJsonEscape,true);
  35760. end;
  35761. procedure StatsAddSizeForCall(Stats: TSynMonitorInputOutput; const Call: TSQLRestURIParams);
  35762. begin
  35763. Stats.AddSize( // rough estimation
  35764. length(Call.Url)+length(Call.Method)+length(Call.InHead)+length(Call.InBody)+12,
  35765. length(Call.OutHead)+length(Call.OutBody)+16);
  35766. end;
  35767. procedure TSQLRestServerURIContext.StatsFromContext(Stats: TSynMonitorInputOutput;
  35768. var Diff: Int64; DiffIsMicroSecs: boolean);
  35769. begin
  35770. StatsAddSizeForCall(Stats,Call^);
  35771. if not StatusCodeIsSuccess(Call.OutStatus) then
  35772. Stats.ProcessErrorNumber(Call.OutStatus);
  35773. if DiffIsMicroSecs then // avoid a division
  35774. Stats.FromExternalMicroSeconds(Diff) else
  35775. Diff := Stats.FromExternalQueryPerformanceCounters(Diff); // converted to us
  35776. end;
  35777. procedure TSQLRestServerURIContext.ExecuteSOAByMethod;
  35778. var timeStart,timeEnd: Int64;
  35779. sessionstat: TSynMonitorInputOutput;
  35780. begin
  35781. with Server.fPublishedMethod[MethodIndex] do begin
  35782. if mlMethods in Server.StatLevels then begin
  35783. QueryPerformanceCounter(timeStart);
  35784. if Stats=nil then
  35785. Stats := TSynMonitorInputOutput.Create(Name);
  35786. Stats.Processing := true;
  35787. end;
  35788. Server.InternalLog('% %',[Name,Parameters],sllServiceCall);
  35789. CallBack(self);
  35790. if Stats<>nil then begin
  35791. QueryPerformanceCounter(timeEnd);
  35792. dec(timeEnd,timeStart);
  35793. StatsFromContext(Stats,timeEnd,false);
  35794. if Server.StatUsage<>nil then
  35795. Server.StatUsage.Modified(Stats,[]);
  35796. if (mlSessions in Server.StatLevels) and (fAuthSession<>nil) then begin
  35797. if fAuthSession.Methods=nil then
  35798. SetLength(fAuthSession.fMethods,length(Server.fPublishedMethod));
  35799. sessionstat := fAuthSession.fMethods[MethodIndex];
  35800. if sessionstat=nil then begin
  35801. sessionstat := TSynMonitorInputOutput.Create(Name);
  35802. fAuthSession.fMethods[MethodIndex] := sessionstat;
  35803. end;
  35804. StatsFromContext(sessionstat,timeEnd,true);
  35805. // mlSessions stats are not yet tracked per Client
  35806. end;
  35807. end;
  35808. end;
  35809. with Server.fStats do begin
  35810. EnterCriticalSection(fLock);
  35811. inc(fServiceMethod);
  35812. Changed;
  35813. LeaveCriticalSection(fLock);
  35814. end;
  35815. end;
  35816. const
  35817. SERVICE_METHODINDEX_FREEINSTANCE = -1;
  35818. procedure TSQLRestServerURIContext.ServiceResultStart(WR: TTextWriter);
  35819. const JSONSTART: array[boolean] of RawUTF8 =
  35820. ('{"result":[','{"result":{');
  35821. begin // InternalExecuteSOAByInterface has set ForceServiceResultAsJSONObject
  35822. if ForceServiceResultAsJSONObjectWithoutResult then
  35823. WR.Add('{') else
  35824. WR.AddString(JSONSTART[ForceServiceResultAsJSONObject]);
  35825. end;
  35826. procedure TSQLRestServerURIContext.ServiceResultEnd(WR: TTextWriter; ID: TID);
  35827. const JSONSEND_WITHID: array[boolean] of RawUTF8 = ('],"id":','},"id":');
  35828. JSONSEND_NOID: array[boolean] of AnsiChar = (']','}');
  35829. begin // InternalExecuteSOAByInterface has set ForceServiceResultAsJSONObject
  35830. if ID=0 then
  35831. WR.Add(JSONSEND_NOID[ForceServiceResultAsJSONObject]) else begin
  35832. if ForceServiceResultAsJSONObjectWithoutResult then
  35833. raise EServiceException.CreateUTF8(
  35834. '%.ServiceResultEnd(ID=%) with ForceServiceResultAsJSONObjectWithoutResult',
  35835. [self,ID]);
  35836. WR.AddString(JSONSEND_WITHID[ForceServiceResultAsJSONObject]);
  35837. WR.Add(ID); // only used in sicClientDriven mode
  35838. end;
  35839. if not ForceServiceResultAsJSONObjectWithoutResult then
  35840. WR.Add('}');
  35841. end;
  35842. procedure TSQLRestServerURIContext.InternalExecuteSOAByInterface;
  35843. procedure ComputeResult;
  35844. procedure ServiceResult(const Name,JSONValue: RawUTF8);
  35845. var WR: TTextWriter;
  35846. begin
  35847. WR := TJSONSerializer.CreateOwnedStream;
  35848. try
  35849. ServiceResultStart(WR);
  35850. if ForceServiceResultAsJSONObject then
  35851. WR.AddFieldName(Name);
  35852. WR.AddString(JSONValue);
  35853. ServiceResultEnd(WR,0);
  35854. Returns(WR.Text);
  35855. finally
  35856. WR.Free;
  35857. end;
  35858. end;
  35859. begin
  35860. ForceServiceResultAsXMLObject := ForceServiceResultAsXMLObject or
  35861. Service.ResultAsXMLObject;
  35862. ForceServiceResultAsJSONObject := ForceServiceResultAsJSONObject or
  35863. Service.ResultAsJSONObject or
  35864. Service.ResultAsJSONObjectWithoutResult or
  35865. ForceServiceResultAsXMLObject; // XML needs a full JSON object as input
  35866. ForceServiceResultAsJSONObjectWithoutResult := ForceServiceResultAsJSONObject and
  35867. (Service.InstanceCreation in SERVICE_IMPLEMENTATION_NOID) and
  35868. Service.ResultAsJSONObjectWithoutResult;
  35869. if ForceServiceResultAsXMLObjectNameSpace='' then
  35870. ForceServiceResultAsXMLObjectNameSpace := Service.ResultAsXMLObjectNameSpace;
  35871. with Server.fStats do begin
  35872. EnterCriticalSection(fLock);
  35873. inc(fServiceInterface);
  35874. Changed;
  35875. LeaveCriticalSection(fLock);
  35876. end;
  35877. case ServiceMethodIndex of
  35878. ord(imFree):
  35879. if not (Service.InstanceCreation in [sicClientDriven..sicPerThread]) then begin
  35880. Error('_free_ is not compatible with %',[ToText(Service.InstanceCreation)^]);
  35881. exit;
  35882. end else // {"method":"_free_", "params":[], "id":1234}
  35883. ServiceMethodIndex := SERVICE_METHODINDEX_FREEINSTANCE;
  35884. ord(imContract): begin
  35885. // "method":"_contract_" to retrieve the implementation contract
  35886. if (Call^.InBody<>'') and (Call^.InBody<>'[]') then
  35887. Server.AssociatedServices.RegisterFromClientJSON(Call^.InBody);
  35888. ServiceResult('contract',Service.ContractExpected);
  35889. exit; // "id":0 for this method -> no instance was created
  35890. end;
  35891. ord(imSignature): begin
  35892. // "method":"_signature_" to retrieve the implementation signature
  35893. if TServiceContainerServer(Server.Services).PublishSignature then
  35894. ServiceResult('signature',Service.Contract) else
  35895. // "id":0 for this method -> no instance was created
  35896. Error('Not allowed to publish signature');
  35897. exit;
  35898. end;
  35899. else begin // TServiceFactoryServer.ExecuteMethod() expects index in fMethods[]:
  35900. dec(ServiceMethodIndex,SERVICE_PSEUDO_METHOD_COUNT);
  35901. if cardinal(ServiceMethodIndex)>=Service.fInterface.fMethodsCount then begin
  35902. Error('Invalid ServiceMethodIndex');
  35903. exit;
  35904. end;
  35905. ServiceExecution := @Service.fExecution[ServiceMethodIndex];
  35906. end;
  35907. end;
  35908. if (Session>CONST_AUTHENTICATION_NOT_USED) and (ServiceExecution<>nil) and
  35909. (SessionGroup-1 in ServiceExecution.Denied) then begin
  35910. Error('Unauthorized method',HTML_NOTALLOWED);
  35911. exit;
  35912. end;
  35913. // if we reached here, we have to run the service method
  35914. Service.ExecuteMethod(self);
  35915. end;
  35916. var xml: RawUTF8;
  35917. m: integer;
  35918. begin // expects Service, ServiceParameters, ServiceMethodIndex to be set
  35919. m := ServiceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT;
  35920. {$ifdef WITHLOG}
  35921. if sllServiceCall in Log.GenericFamily.Level then
  35922. if (m>=0) and (optNoLogInput in Service.fExecution[m].Options) then
  35923. Log.Log(sllServiceCall,'%{optNoLogInput}',[Service.InterfaceFactory.Methods[m].
  35924. InterfaceDotMethodName],Server) else
  35925. Log.Log(sllServiceCall,'%%',[Service.InterfaceFactory.GetFullMethodName(
  35926. ServiceMethodIndex),ServiceParameters],Server);
  35927. {$endif}
  35928. if Assigned(Service.OnMethodExecute) and (m>=0) then
  35929. if not Service.OnMethodExecute(self,Service.InterfaceFactory.Methods[m]) then
  35930. exit; // execution aborted by OnMethodExecute() callback event
  35931. if Service.ResultAsXMLObjectIfAcceptOnlyXML then begin
  35932. xml := FindIniNameValue(pointer(Call^.InHead),'ACCEPT: ');
  35933. if (xml='application/xml') or (xml='text/xml') then
  35934. ForceServiceResultAsXMLObject := true;
  35935. end;
  35936. try
  35937. ComputeResult;
  35938. finally
  35939. ServiceParameters := nil; // ensure no GPF later if points to some local data
  35940. end;
  35941. if ForceServiceResultAsXMLObject and (Call.OutBody<>'') and (Call.OutHead<>'') and
  35942. CompareMem(pointer(Call.OutHead),pointer(JSON_CONTENT_TYPE_HEADER_VAR),45) then begin
  35943. delete(Call.OutHead,15,31);
  35944. insert(XML_CONTENT_TYPE,Call.OutHead,15);
  35945. JSONBufferToXML(pointer(Call.OutBody),XMLUTF8_HEADER,
  35946. ForceServiceResultAsXMLObjectNameSpace,xml);
  35947. Call.OutBody := xml;
  35948. end;
  35949. end;
  35950. procedure TSQLRestServerURIContext.ExecuteORMGet;
  35951. procedure ConvertOutBodyAsPlainJSON(const FieldsCSV: RawUTF8;
  35952. Options: TJSONSerializerSQLRecordOptions);
  35953. var rec: TSQLRecord;
  35954. W: TJSONSerializer;
  35955. bits: TSQLFieldBits;
  35956. withid: boolean;
  35957. begin // force plain standard JSON output for AJAX clients
  35958. if (FieldsCSV='') or
  35959. // handle ID single field only if ID_str is needed
  35960. (IsRowID(pointer(FieldsCSV)) and not (jwoID_str in Options)) or
  35961. // we won't handle min()/max() functions
  35962. not TableRecordProps.Props.FieldBitsFromCSV(FieldsCSV,bits,withid) then
  35963. exit;
  35964. rec := Table.CreateAndFillPrepare(Call.OutBody);
  35965. try
  35966. W := TableRecordProps.Props.CreateJSONWriter(
  35967. TRawByteStringStream.Create,true,FieldsCSV,0);
  35968. try
  35969. include(W.fCustomOptions,twoForceJSONStandard); // force regular JSON
  35970. W.SQLRecordOptions := Options; // will do the magic
  35971. rec.AppendFillAsJsonValues(W);
  35972. W.SetText(Call.OutBody);
  35973. finally
  35974. W.Stream.Free; // associated TRawByteStringStream instance
  35975. W.Free;
  35976. end;
  35977. finally
  35978. rec.Free;
  35979. end;
  35980. end;
  35981. var SQLSelect, SQLWhere, SQLWhereCount, SQLSort, SQLDir, SQL: RawUTF8;
  35982. SQLStartIndex, SQLResults, SQLTotalRowsCount: integer;
  35983. NonStandardSQLSelectParameter, NonStandardSQLWhereParameter: boolean;
  35984. SQLisSelect: boolean;
  35985. ResultList: TSQLTableJSON;
  35986. TableIndexes: TIntegerDynArray;
  35987. rec: TSQLRecord;
  35988. opt: TJSONSerializerSQLRecordOptions;
  35989. P: PUTF8Char;
  35990. i,j,L: integer;
  35991. Blob: PPropInfo;
  35992. begin
  35993. {$ifdef KYLIX3}
  35994. TableIndexes := nil; // make Kylix happy
  35995. {$endif}
  35996. case Method of
  35997. mLOCK,mGET: begin
  35998. if Table=nil then begin
  35999. if (Method<>mLOCK) then begin
  36000. if (Call.InBody='') and (Parameters<>nil) and
  36001. (reUrlEncodedSQL in Call.RestAccessRights^.AllowRemoteExecute) then begin
  36002. // GET with a SQL statement sent in URI, as sql=....
  36003. while not UrlDecodeValue(Parameters,'SQL=',SQL,@Parameters) do
  36004. if Parameters=nil then break;
  36005. end else
  36006. // GET with a SQL statement sent as UTF-8 body (not 100% HTTP compatible)
  36007. SQL := Call.InBody;
  36008. if SQL<>'' then begin
  36009. SQLisSelect := isSelect(pointer(SQL),@SQLSelect);
  36010. if SQLisSelect or
  36011. (reSQL in Call.RestAccessRights^.AllowRemoteExecute) then begin
  36012. Static := nil;
  36013. if SQLisSelect then begin
  36014. TableIndexes := Server.Model.GetTableIndexesFromSQLSelect(SQL);
  36015. if TableIndexes=nil then begin
  36016. // check for SELECT without any known table
  36017. if not (reSQLSelectWithoutTable in
  36018. Call.RestAccessRights^.AllowRemoteExecute) then begin
  36019. Call.OutStatus := HTML_NOTALLOWED;
  36020. exit;
  36021. end;
  36022. end else begin
  36023. // check for SELECT with one (or several JOINed) tables
  36024. for i := 0 to high(TableIndexes) do
  36025. if not (TableIndexes[i] in Call.RestAccessRights^.GET) then begin
  36026. Call.OutStatus := HTML_NOTALLOWED;
  36027. exit;
  36028. end;
  36029. // use the first static table (poorman's JOIN)
  36030. Static := Server.InternalAdaptSQL(TableIndexes[0],SQL);
  36031. end;
  36032. end;
  36033. if Static<>nil then begin
  36034. TableEngine := Static;
  36035. Call.OutBody := TableEngine.EngineList(SQL);
  36036. end else
  36037. Call.OutBody := Server.MainEngineList(SQL,false,nil);
  36038. // security note: only first statement is run by EngineList()
  36039. if Call.OutBody<>'' then begin // got JSON list '[{...}]' ?
  36040. if (SQLSelect<>'') and (length(TableIndexes)=1) then begin
  36041. InternalSetTableFromTableIndex(TableIndexes[0]);
  36042. opt := ClientSQLRecordOptions;
  36043. if opt<>[] then
  36044. ConvertOutBodyAsPlainJSON(SQLSelect,opt);
  36045. end;
  36046. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36047. if not SQLisSelect then // accurate fStats.NotifyORM(Method) below
  36048. Method := TSQLURIMethod(IdemPCharArray(SQLBegin(pointer(SQL)),
  36049. ['INSERT','UPDATE','DELETE'])+2); // -1+2 -> mGET=1
  36050. end;
  36051. end;
  36052. end;
  36053. end;
  36054. end else
  36055. // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1]
  36056. if not (TableIndex in Call.RestAccessRights^.GET) then // check User Access
  36057. Call.OutStatus := HTML_NOTALLOWED else begin
  36058. if TableID>0 then begin
  36059. // GET ModelRoot/TableName/TableID[/BlobFieldName] to retrieve one member,
  36060. // with or w/out locking, or a specified BLOB field content
  36061. if Method=mLOCK then // Safe.Lock is to be followed by PUT -> check user
  36062. if not (TableIndex in Call.RestAccessRights^.PUT) then
  36063. Call.OutStatus := HTML_NOTALLOWED else
  36064. if Server.Model.Lock(TableIndex,TableID) then
  36065. Method := mGET; // mark successfully locked
  36066. if Method<>mLOCK then
  36067. if URIBlobFieldName<>'' then begin
  36068. // GET ModelRoot/TableName/TableID/BlobFieldName: retrieve BLOB field content
  36069. Blob := Table.RecordProps.BlobFieldPropFromRawUTF8(URIBlobFieldName);
  36070. if Blob<>nil then begin
  36071. if TableEngine.EngineRetrieveBlob(TableIndex,
  36072. TableID,Blob,TSQLRawBlob(Call.OutBody)) then begin
  36073. Call.OutHead := GetMimeContentTypeHeader(Call.OutBody);
  36074. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36075. end else
  36076. Call.OutStatus := HTML_NOTFOUND;
  36077. end;
  36078. end else begin
  36079. // GET ModelRoot/TableName/TableID: retrieve a member content, JSON encoded
  36080. Call.OutBody := Server.fCache.Retrieve(TableIndex,TableID);
  36081. if Call.OutBody='' then begin
  36082. // get JSON object '{...}'
  36083. if Static<>nil then
  36084. Call.OutBody := Static.EngineRetrieve(TableIndex,TableID) else
  36085. Call.OutBody := Server.MainEngineRetrieve(TableIndex,TableID);
  36086. // cache if expected
  36087. if Call.OutBody='' then
  36088. Server.fCache.NotifyDeletion(TableIndex,TableID) else
  36089. Server.fCache.Notify(TableIndex,TableID,Call.OutBody,soSelect);
  36090. end;
  36091. if Call.OutBody<>'' then begin // if something was found
  36092. opt := ClientSQLRecordOptions;
  36093. if opt<>[] then begin
  36094. rec := Table.CreateFrom(Call.OutBody); // cached? -> make private
  36095. try
  36096. Call.OutBody := rec.GetJSONValues(true,true,soSelect,nil,opt);
  36097. finally
  36098. rec.Free;
  36099. end;
  36100. end;
  36101. Call.OutStatus := HTML_SUCCESS;
  36102. end else // 200 OK
  36103. Call.OutStatus := HTML_NOTFOUND;
  36104. end;
  36105. end else
  36106. // ModelRoot/TableName with 'select=..&where=' or YUI paging
  36107. if Method<>mLOCK then begin // Safe.Lock not available here
  36108. SQLSelect := 'RowID'; // if no select is specified (i.e. ModelRoot/TableName)
  36109. // all IDs of this table are returned to the client
  36110. SQLTotalRowsCount := 0;
  36111. if Parameters<>nil then begin // '?select=...&where=...' or '?where=...'
  36112. SQLStartIndex := 0;
  36113. SQLResults := 0;
  36114. if Parameters^<>#0 then
  36115. with Server.URIPagingParameters do begin
  36116. NonStandardSQLSelectParameter := StrComp(Select,PAGINGPARAMETERS_YAHOO.Select)<>0;
  36117. NonStandardSQLWhereParameter := StrComp(Where,PAGINGPARAMETERS_YAHOO.Where)<>0;
  36118. repeat
  36119. UrlDecodeValue(Parameters,Sort,SQLSort);
  36120. UrlDecodeValue(Parameters,Dir,SQLDir);
  36121. UrlDecodeInteger(Parameters,StartIndex,SQLStartIndex);
  36122. UrlDecodeInteger(Parameters,Results,SQLResults);
  36123. UrlDecodeValue(Parameters,Select,SQLSelect);
  36124. if NonStandardSQLSelectParameter and (SQLSelect='') then
  36125. UrlDecodeValue(Parameters,PAGINGPARAMETERS_YAHOO.Select,SQLSelect);
  36126. if NonStandardSQLWhereParameter and (SQLWhere='') then
  36127. UrlDecodeValue(Parameters,PAGINGPARAMETERS_YAHOO.Where,SQLWhere);
  36128. UrlDecodeValue(Parameters,Server.URIPagingParameters.Where,SQLWhere,@Parameters);
  36129. until Parameters=nil;
  36130. end;
  36131. // let SQLite3 do the sort and the paging (will be ignored by Static)
  36132. SQLWhereCount := SQLWhere; // "select count(*)" won't expect any ORDER
  36133. if (SQLSort<>'') and
  36134. not ContainsUTF8(pointer(SQLWhere),'ORDER BY') then begin
  36135. if SameTextU(SQLDir,'DESC') then
  36136. SQLSort := SQLSort+' DESC'; // allow DESC, default is ASC
  36137. SQLWhere := SQLWhere+' ORDER BY '+SQLSort;
  36138. end;
  36139. SQLWhere := trim(SQLWhere);
  36140. if (SQLResults<>0) and not ContainsUTF8(pointer(SQLWhere),'LIMIT ') then begin
  36141. if (Server.URIPagingParameters.SendTotalRowsCountFmt<>'') then begin
  36142. if SQLWhere=SQLWhereCount then begin
  36143. i := PosEx('ORDER BY ',UpperCase(SQLWhereCount));
  36144. if i>0 then // if ORDER BY already in the SQLWhere clause
  36145. SetLength(SQLWhereCount,i-1);
  36146. end;
  36147. ResultList := Server.ExecuteList([Table],
  36148. Server.Model.TableProps[TableIndex].SQLFromSelectWhere('Count(*)',SQLWhereCount));
  36149. if ResultList<>nil then
  36150. try
  36151. SQLTotalRowsCount := ResultList.GetAsInteger(1,0);
  36152. finally
  36153. ResultList.Free;
  36154. end;
  36155. end;
  36156. SQLWhere := FormatUTF8('% LIMIT % OFFSET %',[SQLWhere,SQLResults,SQLStartIndex]);
  36157. end;
  36158. end;
  36159. SQL := Server.Model.TableProps[TableIndex].
  36160. SQLFromSelectWhere(SQLSelect,trim(SQLWhere));
  36161. Call.OutBody := Server.InternalListRawUTF8(TableIndex,SQL);
  36162. if Call.OutBody<>'' then begin // got JSON list '[{...}]' ?
  36163. opt := ClientSQLRecordOptions;
  36164. if opt<>[] then
  36165. ConvertOutBodyAsPlainJSON(SQLSelect,opt);
  36166. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36167. if Server.URIPagingParameters.SendTotalRowsCountFmt<>'' then
  36168. // insert "totalRows":% optional value to the JSON output
  36169. if Server.NoAJAXJSON or (ClientKind=ckFramework) then begin
  36170. P := pointer(Call.OutBody);
  36171. L := length(Call.OutBody);
  36172. P := NotExpandedBufferRowCountPos(P,P+L);
  36173. j := 0;
  36174. if P<>nil then
  36175. j := P-pointer(Call.OutBody)-11 else
  36176. for i := 1 to 10 do
  36177. if Call.OutBody[L]='}' then begin
  36178. j := L;
  36179. break;
  36180. end else
  36181. dec(L);
  36182. if j>0 then
  36183. Insert(FormatUTF8(Server.URIPagingParameters.SendTotalRowsCountFmt,
  36184. [SQLTotalRowsCount]),Call.OutBody,j);
  36185. end else begin // expanded format -> as {"values":[...],"total":n}
  36186. if SQLTotalRowsCount=0 then // avoid sending fields array
  36187. Call.OutBody := '[]' else
  36188. Call.OutBody := trim(Call.OutBody);
  36189. Call.OutBody := '{"values":'+Call.OutBody+
  36190. FormatUTF8(Server.URIPagingParameters.SendTotalRowsCountFmt,[SQLTotalRowsCount])+'}';
  36191. end;
  36192. end else
  36193. Call.OutStatus := HTML_NOTFOUND;
  36194. end;
  36195. end;
  36196. if Call.OutStatus=HTML_SUCCESS then
  36197. Server.fStats.NotifyORM(Method);
  36198. end;
  36199. mUNLOCK: begin
  36200. // ModelRoot/TableName/TableID to unlock a member
  36201. if not (TableIndex in Call.RestAccessRights^.PUT) then
  36202. Call.OutStatus := HTML_NOTALLOWED else
  36203. if (Table<>nil) and (TableID>0) and
  36204. Server.Model.UnLock(Table,TableID) then
  36205. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36206. end;
  36207. mSTATE: begin
  36208. // STATE method for TSQLRestClientServerInternalState
  36209. // this method is called with Root (-> Table=nil -> Static=nil)
  36210. // we need a specialized method in order to avoid fStats.Invalid increase
  36211. Call.OutStatus := HTML_SUCCESS;
  36212. for i := 0 to high(Server.fStaticData) do
  36213. if (Server.fStaticData[i]<>nil) and
  36214. Server.fStaticData[i].InheritsFrom(TSQLRestStorage) then
  36215. if TSQLRestStorage(Server.fStaticData[i]).RefreshedAndModified then begin
  36216. inc(Server.InternalState); // force refresh
  36217. break;
  36218. end;
  36219. end else
  36220. raise EORMException.CreateUTF8('%.ExecuteORMGet(method=%)',[self,ord(Method)]);
  36221. end;
  36222. end;
  36223. procedure TSQLRestServerURIContext.ExecuteORMWrite;
  36224. procedure ComputeInBodyFields(Occasion: TSQLEvent);
  36225. var Rec: TSQLRecord;
  36226. bits: TSQLFieldBits;
  36227. begin
  36228. Rec := Table.Create;
  36229. try
  36230. Rec.FillFrom(pointer(Call.InBody),@bits);
  36231. Rec.ComputeFieldsBeforeWrite(Server,Occasion);
  36232. with TableRecordProps.Props do
  36233. if Occasion=seAdd then
  36234. bits := bits+ComputeBeforeAddFieldsBits else
  36235. bits := bits+ComputeBeforeUpdateFieldsBits;
  36236. Call.Inbody := Rec.GetJSONValues(true,Rec.IDValue<>0,bits);
  36237. finally
  36238. Rec.Free;
  36239. end;
  36240. end;
  36241. var OK: boolean;
  36242. Blob: PPropInfo;
  36243. SQLSelect, SQLWhere, SQLSort, SQLDir: RawUTF8;
  36244. begin
  36245. if MethodIndex=Server.fPublishedMethodBatchIndex then begin
  36246. ExecuteSOAByMethod; // run the BATCH process in execORMWrite context
  36247. exit;
  36248. end;
  36249. if not Call.RestAccessRights^.CanExecuteORMWrite(
  36250. Method,Table,TableIndex,TableID,self) then begin
  36251. Call.OutStatus := HTML_FORBIDDEN;
  36252. exit;
  36253. end;
  36254. case Method of
  36255. mPOST: // POST=ADD=INSERT
  36256. if Table=nil then begin
  36257. // ModelRoot with free SQL statement sent as UTF-8 (only for Admin group)
  36258. // see e.g. TSQLRestClientURI.EngineExecute
  36259. if reSQL in Call.RestAccessRights^.AllowRemoteExecute then
  36260. if (Call.InBody<>'') and
  36261. (not (GotoNextNotSpace(Pointer(Call.InBody))^ in [#0,'[','{'])) and
  36262. Server.EngineExecute(Call.InBody) then begin
  36263. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36264. end else
  36265. Call.OutStatus := HTML_FORBIDDEN;
  36266. end else begin
  36267. // ModelRoot/TableName with possible JSON SentData: create a new member
  36268. // here, Table<>nil, TableID<0 and TableIndex in [0..MAX_SQLTABLES-1]
  36269. if rsoComputeFieldsBeforeWriteOnServerSide in Server.Options then
  36270. ComputeInBodyFields(seAdd);
  36271. TableID := TableEngine.EngineAdd(TableIndex,Call.InBody);
  36272. if TableID<>0 then begin
  36273. Call.OutStatus := HTML_CREATED; // 201 Created
  36274. Call.OutHead := 'Location: '+URI+'/'+Int64ToUtf8(TableID);
  36275. if rsoAddUpdateReturnsContent in Server.Options then begin
  36276. Server.fCache.NotifyDeletion(TableIndex,TableID);
  36277. Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
  36278. Server.fCache.Notify(TableIndex,TableID,Call.OutBody,soInsert);
  36279. end else
  36280. Server.fCache.Notify(TableIndex,TableID,Call.InBody,soInsert);
  36281. end;
  36282. end;
  36283. mPUT: // PUT=UPDATE
  36284. if TableID>0 then begin
  36285. // PUT ModelRoot/TableName/TableID[/BlobFieldName] to update member/BLOB content
  36286. if Server.RecordCanBeUpdated(Table,TableID,seUpdate,@CustomErrorMsg) then begin
  36287. OK := false;
  36288. if URIBlobFieldName<>'' then begin
  36289. // PUT ModelRoot/TableName/TableID/BlobFieldName: update BLOB field content
  36290. Blob := Table.RecordProps.BlobFieldPropFromRawUTF8(URIBlobFieldName);
  36291. if Blob<>nil then
  36292. OK := TableEngine.EngineUpdateBlob(TableIndex,TableID,Blob,Call.InBody);
  36293. end else begin
  36294. // ModelRoot/TableName/TableID with JSON SentData: update a member
  36295. if rsoComputeFieldsBeforeWriteOnServerSide in Server.Options then
  36296. ComputeInBodyFields(seUpdate);
  36297. OK := TableEngine.EngineUpdate(TableIndex,TableID,Call.InBody);
  36298. if OK then begin // flush (no CreateTime in JSON)
  36299. Server.fCache.NotifyDeletion(TableIndex,TableID);
  36300. if rsoAddUpdateReturnsContent in Server.Options then
  36301. Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
  36302. end;
  36303. end;
  36304. if OK then
  36305. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36306. end else
  36307. Call.OutStatus := HTML_FORBIDDEN;
  36308. end else
  36309. if Parameters<>nil then begin // e.g. from TSQLRestClient.EngineUpdateField
  36310. // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=..
  36311. repeat
  36312. UrlDecodeValue(Parameters,'SETNAME=',SQLSelect);
  36313. UrlDecodeValue(Parameters,'SET=',SQLDir);
  36314. UrlDecodeValue(Parameters,'WHERENAME=',SQLSort);
  36315. UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters);
  36316. until Parameters=nil;
  36317. if (SQLSelect<>'') and (SQLDir<>'') and (SQLSort<>'') and (SQLWhere<>'') then
  36318. if TableEngine.EngineUpdateField(TableIndex,SQLSelect,SQLDir,SQLSort,SQLWhere) then begin
  36319. if rsoAddUpdateReturnsContent in Server.Options then
  36320. Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
  36321. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36322. end;
  36323. end;
  36324. mDELETE:
  36325. if TableID>0 then
  36326. // ModelRoot/TableName/TableID to delete a member
  36327. if not Server.RecordCanBeUpdated(Table,TableID,seDelete,@CustomErrorMsg) then
  36328. Call.OutStatus := HTML_FORBIDDEN else begin
  36329. if TableEngine.EngineDelete(TableIndex,TableID) and
  36330. Server.AfterDeleteForceCoherency(TableIndex,TableID) then begin
  36331. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36332. Server.fCache.NotifyDeletion(TableIndex,TableID);
  36333. end;
  36334. end else
  36335. if Parameters<>nil then begin
  36336. // ModelRoot/TableName?where=WhereClause to delete members
  36337. repeat
  36338. if UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters) then begin
  36339. SQLWhere := trim(SQLWhere);
  36340. if SQLWhere<>'' then begin
  36341. if Server.Delete(Table,SQLWhere) then
  36342. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36343. end;
  36344. break;
  36345. end;
  36346. until Parameters=nil;
  36347. end;
  36348. mBEGIN: begin // BEGIN TRANSACTION
  36349. // TSQLVirtualTableJSON/External will rely on SQLite3 module
  36350. // and also TSQLRestStorageInMemory, since COMMIT/ROLLBACK have Static=nil
  36351. // mBEGIN logic is just the opposite of mEND/mABORT: Safe.Lock main, then static
  36352. if Server.TransactionBegin(Table,Session) then begin
  36353. if (Static<>nil) and (StaticKind=sVirtualTable) then
  36354. Static.TransactionBegin(Table,Session) else
  36355. if (Static=nil) and (Server.fTransactionTable<>nil) then begin
  36356. Static := Server.StaticVirtualTable[Server.fTransactionTable];
  36357. if Static<>nil then
  36358. Static.TransactionBegin(Table,Session);
  36359. end;
  36360. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36361. end;
  36362. end;
  36363. mEND: begin // END=COMMIT
  36364. // this method is called with Root (-> Table=nil -> Static=nil)
  36365. // mEND logic is just the opposite of mBEGIN: release static, then main
  36366. if (Static<>nil) and (StaticKind=sVirtualTable) then
  36367. Static.Commit(Session,false) else
  36368. if (Static=nil) and (Server.fTransactionTable<>nil) then begin
  36369. Static := Server.StaticVirtualTable[Server.fTransactionTable];
  36370. if Static<>nil then
  36371. Static.Commit(Session,false);
  36372. end;
  36373. Server.Commit(Session,false);
  36374. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36375. end;
  36376. mABORT: begin // ABORT=ROLLBACK
  36377. // this method is called with Root (-> Table=nil -> Static=nil)
  36378. // mABORT logic is just the opposite of mBEGIN: release static, then main
  36379. if (Static<>nil) and (StaticKind=sVirtualTable) then
  36380. Static.RollBack(Session) else
  36381. if (Static=nil) and (Server.fTransactionTable<>nil) then begin
  36382. Static := Server.StaticVirtualTable[Server.fTransactionTable];
  36383. if Static<>nil then
  36384. Static.RollBack(Session);
  36385. end;
  36386. Server.RollBack(Session);
  36387. Call.OutStatus := HTML_SUCCESS; // 200 OK
  36388. end;
  36389. end;
  36390. if StatusCodeIsSuccess(Call.OutStatus) then
  36391. Server.fStats.NotifyORM(Method);
  36392. end;
  36393. procedure TSQLRestServerURIContext.FillInput(const LogInputIdent: RawUTF8);
  36394. var n,max: integer;
  36395. P: PUTF8Char;
  36396. begin
  36397. if (fInput<>nil) or (Parameters=nil) then
  36398. exit; // only do it once
  36399. P := Parameters;
  36400. n := 0;
  36401. max := 0;
  36402. repeat
  36403. if n>=max then begin
  36404. if n>=96 then // avoid DOS - see MAX_METHOD_ARGS for TInterfacedObjectFake
  36405. raise EParsingException.CreateUTF8(
  36406. 'Security Policy: Accept up to 48 parameters for %.FillInput',[self]);
  36407. inc(max,16);
  36408. SetLength(fInput,max);
  36409. end;
  36410. P := UrlDecodeNextNameValue(P,fInput[n],fInput[n+1]);
  36411. if P=nil then
  36412. break;
  36413. inc(n,2);
  36414. until P^=#0;
  36415. SetLength(fInput,n);
  36416. {$ifdef WITHLOG}
  36417. if LogInputIdent<>'' then
  36418. Log.Add.Log(sllDebug,LogInputIdent,TypeInfo(TRawUTF8DynArray),fInput,self);
  36419. {$endif}
  36420. end;
  36421. function TSQLRestServerURIContext.GetInputInt(const ParamName: RawUTF8): Int64;
  36422. var err: integer;
  36423. begin
  36424. result := GetInt64(pointer(GetInputUTF8(ParamName)),err);
  36425. if err<>0 then
  36426. raise EParsingException.CreateUTF8('%.GetInputInt(%): Invalid parameter',
  36427. [self,ParamName]);
  36428. end;
  36429. function TSQLRestServerURIContext.GetInputDouble(const ParamName: RawUTF8): double;
  36430. var err: integer;
  36431. begin
  36432. result := GetExtended(pointer(GetInputUTF8(ParamName)),err);
  36433. if err<>0 then
  36434. raise EParsingException.CreateUTF8('%.GetInputDouble(%): Invalid parameter',
  36435. [self,ParamName]);
  36436. end;
  36437. function TSQLRestServerURIContext.GetInputIntOrVoid(const ParamName: RawUTF8): Int64;
  36438. begin
  36439. result := GetInt64(pointer(GetInputUTF8OrVoid(ParamName)));
  36440. end;
  36441. function TSQLRestServerURIContext.GetInputHexaOrVoid(const ParamName: RawUTF8): cardinal;
  36442. var value: RawUTF8;
  36443. begin
  36444. value := GetInputUTF8OrVoid(ParamName);
  36445. if (length(value)<>8) or not HexDisplayToCardinal(Pointer(value),result) then
  36446. result := 0;
  36447. end;
  36448. function TSQLRestServerURIContext.GetInputDoubleOrVoid(const ParamName: RawUTF8): double;
  36449. begin
  36450. result := GetExtended(pointer(GetInputUTF8OrVoid(ParamName)));
  36451. end;
  36452. function TSQLRestServerURIContext.GetInputNameIndex(const ParamName: RawUTF8): integer;
  36453. begin // fInput[0]='Param1',fInput[1]='Value1',fInput[2]='Param2'...
  36454. if (fInput=nil) and (Parameters<>nil) then
  36455. FillInput;
  36456. for result := 0 to (length(fInput)shr 1)-1 do
  36457. if IdemPropNameU(ParamName,fInput[result*2]) then
  36458. exit;
  36459. result := -1;
  36460. end;
  36461. function TSQLRestServerURIContext.GetInputUTF8(const ParamName: RawUTF8): RawUTF8;
  36462. var i: integer;
  36463. begin
  36464. i := GetInputNameIndex(ParamName);
  36465. if i<0 then
  36466. raise EParsingException.CreateUTF8('%: missing ''%'' parameter',[self,ParamName]);
  36467. result := fInput[i*2+1];
  36468. end;
  36469. function TSQLRestServerURIContext.GetInputUTF8OrVoid(const ParamName: RawUTF8): RawUTF8;
  36470. var i: integer;
  36471. begin
  36472. i := GetInputNameIndex(ParamName);
  36473. if i<0 then
  36474. result := '' else
  36475. result := fInput[i*2+1];
  36476. end;
  36477. function TSQLRestServerURIContext.InputUTF8OrDefault(
  36478. const ParamName, DefaultValue: RawUTF8): RawUTF8;
  36479. var i: integer;
  36480. begin
  36481. i := GetInputNameIndex(ParamName);
  36482. if i<0 then
  36483. result := DefaultValue else
  36484. result := fInput[i*2+1];
  36485. end;
  36486. function TSQLRestServerURIContext.InputUTF8OrError(const ParamName: RawUTF8;
  36487. out Value: RawUTF8; const ErrorMessageForMissingParameter: string): boolean;
  36488. var i: integer;
  36489. begin
  36490. i := GetInputNameIndex(ParamName);
  36491. if i<0 then begin
  36492. if ErrorMessageForMissingParameter='' then
  36493. Error('%: missing ''%'' parameter',[self,ParamName]) else
  36494. Error('%',[ErrorMessageForMissingParameter]);
  36495. result := false;
  36496. end else begin
  36497. Value := fInput[i*2+1];
  36498. result := true;
  36499. end;
  36500. end;
  36501. function TSQLRestServerURIContext.InputEnum(const ParamName: RawUTF8;
  36502. EnumType: PTypeInfo; out ValueEnum; DefaultEnumOrd: integer): boolean;
  36503. var value: RawUTF8;
  36504. int,err: Integer;
  36505. begin
  36506. result := false;
  36507. if (EnumType=nil) or (EnumType^.Kind<>tkEnumeration) then
  36508. exit;
  36509. value := GetInputUTF8OrVoid(ParamName);
  36510. if value<>'' then begin
  36511. int := GetInteger(Pointer(value),err);
  36512. if err=0 then
  36513. result := true else begin
  36514. int := EnumType^.EnumBaseType^.GetEnumNameValue(pointer(value),length(value));
  36515. if int>=0 then
  36516. result := true else
  36517. int := DefaultEnumOrd;
  36518. end;
  36519. end else
  36520. int := DefaultEnumOrd;
  36521. EnumType^.EnumBaseType^.SetEnumFromOrdinal(ValueEnum,int);
  36522. end;
  36523. function TSQLRestServerURIContext.GetInputString(const ParamName: RawUTF8): string;
  36524. var i: integer;
  36525. begin
  36526. i := GetInputNameIndex(ParamName);
  36527. if i<0 then
  36528. raise EParsingException.CreateUTF8('%: missing ''%'' parameter',[self,ParamName]);
  36529. result := UTF8ToString(fInput[i*2+1]);
  36530. end;
  36531. function TSQLRestServerURIContext.GetInputStringOrVoid(const ParamName: RawUTF8): string;
  36532. var i: integer;
  36533. begin
  36534. i := GetInputNameIndex(ParamName);
  36535. if i<0 then
  36536. result := '' else
  36537. result := UTF8ToString(fInput[i*2+1]);
  36538. end;
  36539. function TSQLRestServerURIContext.GetInputExists(const ParamName: RawUTF8): Boolean;
  36540. begin
  36541. result := GetInputNameIndex(ParamName)>=0;
  36542. end;
  36543. {$ifndef NOVARIANTS}
  36544. function TSQLRestServerURIContext.GetInput(const ParamName: RawUTF8): variant;
  36545. begin
  36546. GetVariantFromJSON(pointer(GetInputUTF8(ParamName)),false,Result);
  36547. end;
  36548. function TSQLRestServerURIContext.GetInputOrVoid(const ParamName: RawUTF8): variant;
  36549. begin
  36550. GetVariantFromJSON(pointer(GetInputUTF8OrVoid(ParamName)),false,Result);
  36551. end;
  36552. function TSQLRestServerURIContext.InputOrError(const ParamName: RawUTF8;
  36553. out Value: variant; const ErrorMessageForMissingParameter: string): boolean;
  36554. var ValueUTF8: RawUTF8;
  36555. begin
  36556. result := InputUTF8OrError(ParamName,ValueUTF8,ErrorMessageForMissingParameter);
  36557. if result then
  36558. GetVariantFromJSON(pointer(ValueUTF8),False,Value);
  36559. end;
  36560. function TSQLRestServerURIContext.GetInputAsTDocVariant: variant;
  36561. var ndx: integer;
  36562. v: variant;
  36563. MultiPart: TMultiPartDynArray;
  36564. begin
  36565. VarClear(result);
  36566. FillInput;
  36567. if fInput<>nil then begin
  36568. with TDocVariantData(result) do begin
  36569. InitFast;
  36570. for ndx := 0 to (length(fInput) shr 1)-1 do begin
  36571. GetVariantFromJSON(pointer(fInput[ndx*2+1]),false,v,@JSON_OPTIONS[true]);
  36572. AddValue(fInput[ndx*2],v);
  36573. end;
  36574. end;
  36575. end else
  36576. if InputAsMultiPart(MultiPart) then
  36577. with TDocVariantData(result) do begin
  36578. InitFast;
  36579. for ndx := 0 to high(MultiPart) do
  36580. with MultiPart[ndx] do
  36581. if ContentType=TEXT_CONTENT_TYPE then begin
  36582. // append as regular "Name":"TextValue" field
  36583. RawUTF8ToVariant(Content,v);
  36584. AddValue(Name,v);
  36585. end else
  36586. // append binary file as an object, with Base64-encoded data
  36587. AddValue(Name,_ObjFast(['data',BinToBase64(Content),
  36588. 'filename',FileName,'contenttype',ContentType]));
  36589. end;
  36590. end;
  36591. {$endif NOVARIANTS}
  36592. function TSQLRestServerURIContext.InputAsMultiPart(var MultiPart: TMultiPartDynArray): Boolean;
  36593. begin
  36594. result := (Method=mPOST) and
  36595. IdemPChar(pointer(fInputPostContentType),'MULTIPART/FORM-DATA') and
  36596. MultiPartFormDataDecode(fInputPostContentType,Call^.InBody,MultiPart);
  36597. end;
  36598. function TSQLRestServerURIContext.GetInHeader(const HeaderName: RawUTF8): RawUTF8;
  36599. var up: array[byte] of AnsiChar;
  36600. begin
  36601. if self=nil then
  36602. result := '' else begin
  36603. PWord(UpperCopy255(up,HeaderName))^ := ord(':');
  36604. result := Trim(FindIniNameValue(pointer(Call.InHead),up));
  36605. if (result='') and (SessionRemoteIP<>'') and IdemPropNameU(HeaderName,'remoteip') then
  36606. // some protocols (e.g. WebSockets) do not send headers at each call
  36607. result := SessionRemoteIP;
  36608. end;
  36609. end;
  36610. procedure TSQLRestServerURIContext.SetInCookie(CookieName, CookieValue: RawUTF8);
  36611. var i,n: integer;
  36612. begin
  36613. GetInCookie(CookieName); // force retrieve cookies
  36614. fInputCookieLastName := ''; // cache reset
  36615. CookieName := UpperCase(trim(CookieName))+'=';
  36616. n := length(fInputCookies);
  36617. for i := 0 to n-1 do
  36618. if IdemPChar(pointer(fInputCookies[i]),pointer(CookieName)) then begin
  36619. fInputCookies[i] := CookieName+CookieValue; // update in-place
  36620. exit;
  36621. end;
  36622. SetLength(fInputCookies,n+1);
  36623. fInputCookies[n] := CookieName+CookieValue; // add new cookie
  36624. end;
  36625. function TSQLRestServerURIContext.GetInCookie(CookieName: RawUTF8): RawUTF8;
  36626. var i: integer;
  36627. cookieSearch: RawUTF8;
  36628. begin
  36629. result := '';
  36630. CookieName := trim(CookieName);
  36631. if (self=nil) or (CookieName='') then
  36632. exit;
  36633. if CookieName=fInputCookieLastName then begin
  36634. result := fInputCookieLastValue;
  36635. exit;
  36636. end;
  36637. if not fInputCookiesRetrieved then begin
  36638. fInputCookiesRetrieved := true;
  36639. CSVToRawUTF8DynArray(pointer(GetInHeader('cookie')),fInputCookies,';');
  36640. for i := 0 to length(fInputCookies)-1 do
  36641. fInputCookies[i] := trim(fInputCookies[i]);
  36642. end;
  36643. fInputCookieLastName := CookieName;
  36644. fInputCookieLastValue := '';
  36645. if fInputCookies=nil then
  36646. exit;
  36647. cookieSearch := UpperCase(CookieName)+'=';
  36648. for i := 0 to length(fInputCookies)-1 do
  36649. if IdemPChar(pointer(fInputCookies[i]),pointer(cookieSearch)) then begin
  36650. result := copy(fInputCookies[i],length(cookieSearch)+1,MaxInt);
  36651. fInputCookieLastValue := result;
  36652. exit;
  36653. end;
  36654. end;
  36655. procedure TSQLRestServerURIContext.SetOutSetCookie(aOutSetCookie: RawUTF8);
  36656. begin
  36657. if self=nil then
  36658. exit;
  36659. aOutSetCookie := Trim(aOutSetCookie);
  36660. if PosEx('=',aOutSetCookie)<2 then
  36661. raise EBusinessLayerException.CreateUTF8(
  36662. '"name=value" expected for %.SetOutSetCookie("%")',[self,aOutSetCookie]);
  36663. if PosI('; PATH=',aOutSetCookie)=0 then
  36664. fOutSetCookie := aOutSetCookie+'; Path=/'+Server.Model.Root else
  36665. fOutSetCookie := aOutSetCookie;
  36666. fInputCookieLastName := ''; // cache reset
  36667. end;
  36668. function TSQLRestServerURIContext.GetUserAgent: RawUTF8;
  36669. begin
  36670. if fUserAgent='' then begin
  36671. result := FindIniNameValue(pointer(Call.InHead),'USER-AGENT: ');
  36672. if result='' then
  36673. fUserAgent := '*' else // ensure header is parsed only once
  36674. fUserAgent := result;
  36675. end else
  36676. if fUserAgent='*' then
  36677. result := '' else
  36678. result := fUserAgent;
  36679. end;
  36680. function TSQLRestServerURIContext.ClientKind: TSQLRestServerURIContextClientKind;
  36681. var agent: RawUTF8;
  36682. begin
  36683. if fClientKind=ckUnknown then
  36684. if Call.InHead='' then // e.g. for WebSockets remote access
  36685. fClientKind := ckAjax else begin
  36686. agent := GetUserAgent;
  36687. if (agent='') or (PosEx('mORMot',agent)>0) then
  36688. fClientKind := ckFramework else
  36689. fClientKind := ckAjax;
  36690. end;
  36691. result := fClientKind;
  36692. end;
  36693. function TSQLRestServerURIContext.IsRemoteAdministrationExecute: boolean;
  36694. begin
  36695. result := (self<>nil) and (call.RestAccessRights=@BYPASS_ACCESS_RIGHTS);
  36696. end;
  36697. function TSQLRestServerURIContext.ClientSQLRecordOptions: TJSONSerializerSQLRecordOptions;
  36698. begin
  36699. result := [];
  36700. if (TableRecordProps=nil) or (ClientKind<>ckAjax) then
  36701. exit;
  36702. if rsoGetID_str in Server.Options then
  36703. include(result,jwoID_str);
  36704. if ([sftObject,sftBlobDynArray{$ifndef NOVARIANTS},sftVariant{$endif}]*
  36705. TableRecordProps.Props.HasTypeFields<>[]) and
  36706. (rsoGetAsJsonNotAsString in Server.Options) then
  36707. include(result,jwoAsJsonNotAsString);
  36708. end;
  36709. function TSQLRestServerURIContext.GetResourceFileName: TFileName;
  36710. begin
  36711. if (URIBlobFieldName='') or (PosEx('..',URIBlobFieldName)>0) then
  36712. result := '' else // for security, disallow .. in the supplied file path
  36713. result := UTF8ToString(StringReplaceAll(URIBlobFieldName,'/',PathDelim));
  36714. end;
  36715. procedure TSQLRestServerURIContext.Returns(const Result: RawUTF8;
  36716. Status: integer; const CustomHeader: RawUTF8;
  36717. Handle304NotModified,HandleErrorAsRegularResult: boolean);
  36718. var clientHash, serverHash: RawUTF8;
  36719. begin
  36720. if HandleErrorAsRegularResult or StatusCodeIsSuccess(Status) then begin
  36721. Call.OutStatus := Status;
  36722. Call.OutBody := Result;
  36723. if CustomHeader<>'' then
  36724. Call.OutHead := CustomHeader else
  36725. if Call.OutHead='' then
  36726. Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR;
  36727. if Handle304NotModified and (Status=HTML_SUCCESS) and
  36728. (Length(Result)>64) then begin
  36729. clientHash := FindIniNameValue(pointer(Call.InHead),'IF-NONE-MATCH: ');
  36730. serverHash := '"'+crc32cUTF8ToHex(Result)+'"';
  36731. if clientHash<>serverHash then
  36732. Call.OutHead := Call.OutHead+#13#10'ETag: '+serverHash else begin
  36733. Call.OutBody := ''; // save bandwidth for "304 Not Modified"
  36734. Call.OutStatus := HTML_NOTMODIFIED;
  36735. end;
  36736. end;
  36737. end else
  36738. Error(Result,Status);
  36739. end;
  36740. procedure TSQLRestServerURIContext.Returns(Value: TObject; Status: integer;
  36741. Handle304NotModified: boolean; SQLRecordOptions: TJSONSerializerSQLRecordOptions);
  36742. var json: RawUTF8;
  36743. begin
  36744. if Value.InheritsFrom(TSQLRecord) then
  36745. json := TSQLRecord(Value).GetJSONValues(true,true,soSelect,nil,SQLRecordOptions) else
  36746. json := ObjectToJSON(Value);
  36747. Returns(json,Status,'',Handle304NotModified);
  36748. end;
  36749. procedure TSQLRestServerURIContext.ReturnsJson(const Value: Variant; Status: integer;
  36750. Handle304NotModified: boolean; Escape: TTextWriterKind; MakeHumanReadable: boolean);
  36751. var json,tmp: RawUTF8;
  36752. begin
  36753. VariantSaveJSON(Value,Escape,json);
  36754. if MakeHumanReadable and (json<>'') and (json[1] in ['{','[']) then begin
  36755. tmp := json;
  36756. JSONBufferReformat(pointer(tmp),json);
  36757. end;
  36758. Returns(json,Status,'',Handle304NotModified);
  36759. end;
  36760. procedure TSQLRestServerURIContext.ReturnBlob(const Blob: RawByteString;
  36761. Status: integer; Handle304NotModified: boolean; const FileName: TFileName);
  36762. begin
  36763. Returns(Blob,Status,GetMimeContentTypeHeader(Blob,FileName),Handle304NotModified);
  36764. end;
  36765. procedure TSQLRestServerURIContext.ReturnFile(const FileName: TFileName;
  36766. Handle304NotModified: boolean; const ContentType,AttachmentFileName,
  36767. Error404Redirect: RawUTF8);
  36768. var FileTime: TDateTime;
  36769. clientHash, serverHash: RawUTF8;
  36770. begin
  36771. if FileName='' then
  36772. FileTime := 0 else
  36773. FileTime := FileAgeToDateTime(FileName);
  36774. if FileTime=0 then
  36775. if Error404Redirect<>'' then
  36776. Redirect(Error404Redirect) else
  36777. Error('',HTML_NOTFOUND) else begin
  36778. if Call.OutHead<>'' then
  36779. Call.OutHead := Call.OutHead+#13#10;
  36780. if ContentType<>'' then
  36781. Call.OutHead := Call.OutHead+HEADER_CONTENT_TYPE+ContentType else
  36782. Call.OutHead := Call.OutHead+GetMimeContentTypeHeader('',FileName);
  36783. Call.OutStatus := HTML_SUCCESS;
  36784. if Handle304NotModified then begin
  36785. clientHash := FindIniNameValue(pointer(Call.InHead),'IF-NONE-MATCH: ');
  36786. serverHash := '"'+DateTimeToIso8601(FileTime,false)+'"';
  36787. Call.OutHead := Call.OutHead+#13#10'ETag: '+serverHash;
  36788. if clientHash=serverHash then begin
  36789. Call.OutStatus := HTML_NOTMODIFIED;
  36790. exit;
  36791. end;
  36792. end;
  36793. // Content-Type: appears twice: 1st to notify static file, 2nd for mime type
  36794. Call.OutHead := STATICFILE_CONTENT_TYPE_HEADER+#13#10+Call.OutHead;
  36795. StringToUTF8(FileName,Call.OutBody); // body=filename for STATICFILE_CONTENT
  36796. if AttachmentFileName<>'' then
  36797. Call.OutHead := Call.OutHead+
  36798. #13#10'Content-Disposition: attachment; filename="'+AttachmentFileName+'"';
  36799. end;
  36800. end;
  36801. procedure TSQLRestServerURIContext.ReturnFileFromFolder(const FolderName: TFileName;
  36802. Handle304NotModified: boolean; const DefaultFileName: TFileName;
  36803. const Error404Redirect: RawUTF8);
  36804. var fileName: TFileName;
  36805. begin
  36806. if URIBlobFieldName='' then
  36807. fileName := DefaultFileName else
  36808. if PosEx('..',URIBlobFieldName)>0 then
  36809. fileName := '' else
  36810. fileName := UTF8ToString(StringReplaceChars(URIBlobFieldName,'/',PathDelim));
  36811. if fileName<>'' then
  36812. fileName := IncludeTrailingPathDelimiter(FolderName)+fileName;
  36813. ReturnFile(fileName,Handle304NotModified,'','',Error404Redirect);
  36814. end;
  36815. procedure TSQLRestServerURIContext.Redirect(const NewLocation: RawUTF8;
  36816. PermanentChange: boolean);
  36817. begin
  36818. if PermanentChange then
  36819. Call.OutStatus := HTML_MOVEDPERMANENTLY else
  36820. Call.OutStatus := HTML_TEMPORARYREDIRECT;
  36821. Call.OutHead := 'Location: '+NewLocation;
  36822. end;
  36823. procedure TSQLRestServerURIContext.Returns(const NameValuePairs: array of const;
  36824. Status: integer; Handle304NotModified,HandleErrorAsRegularResult: boolean);
  36825. begin
  36826. Returns(JSONEncode(NameValuePairs),Status,'',Handle304NotModified,
  36827. HandleErrorAsRegularResult);
  36828. end;
  36829. procedure TSQLRestServerURIContext.Results(const Values: array of const;
  36830. Status: integer; Handle304NotModified: boolean);
  36831. var i,h: integer;
  36832. result: RawUTF8;
  36833. begin
  36834. h := high(Values);
  36835. if h<0 then
  36836. result := '{"result":null}' else
  36837. with TJSONSerializer.CreateOwnedStream do
  36838. try
  36839. AddShort('{"result":');
  36840. if h=0 then
  36841. // result is one value
  36842. AddJSONEscape(Values[0]) else begin
  36843. // result is one array of values
  36844. Add('[');
  36845. i := 0;
  36846. repeat
  36847. AddJSONEscape(Values[i]);
  36848. if i=h then break;
  36849. Add(',');
  36850. inc(i);
  36851. until false;
  36852. Add(']');
  36853. end;
  36854. Add('}');
  36855. SetText(result);
  36856. finally
  36857. Free;
  36858. end;
  36859. Returns(result,Status,'',Handle304NotModified);
  36860. end;
  36861. procedure TSQLRestServerURIContext.Success(Status: integer);
  36862. begin
  36863. if StatusCodeIsSuccess(Status) then
  36864. Call.OutStatus := Status else
  36865. Error('',Status);
  36866. end;
  36867. procedure TSQLRestServerURIContext.Error(const Format: RawUTF8;
  36868. const Args: array of const; Status: integer);
  36869. begin
  36870. Error(FormatUTF8(Format,Args),Status);
  36871. end;
  36872. procedure TSQLRestServerURIContext.Error(E: Exception;
  36873. const Format: RawUTF8; const Args: array of const; Status: integer);
  36874. var msg,exc: RawUTF8;
  36875. begin
  36876. FormatUTF8(Format,Args,msg);
  36877. if E=nil then
  36878. Error(msg,Status) else begin
  36879. exc := ObjectToJSONDebug(E);
  36880. if msg='' then
  36881. Error('{"%":%}',[E,exc],Status) else
  36882. Error(FormatUTF8('{"msg":?,"%":%}',[E,exc],[msg],true),Status);
  36883. end;
  36884. end;
  36885. procedure TSQLRestServerURIContext.Error(const ErrorMessage: RawUTF8; Status: integer);
  36886. var ErrorMsg: RawUTF8;
  36887. begin
  36888. Call.OutStatus := Status;
  36889. if StatusCodeIsSuccess(Status) then begin // not an error
  36890. Call.OutBody := ErrorMessage;
  36891. exit;
  36892. end;
  36893. if ErrorMessage='' then
  36894. StatusCodeToErrorMsg(Status,ErrorMsg) else
  36895. ErrorMsg := ErrorMessage;
  36896. with TTextWriter.CreateOwnedStream do
  36897. try
  36898. AddShort('{'#13#10'"errorCode":');
  36899. Add(call.OutStatus);
  36900. if (ErrorMsg<>'') and (ErrorMsg[1]='{') and (ErrorMsg[length(ErrorMsg)]='}') then begin
  36901. AddShort(','#13#10'"error":'#13#10);
  36902. AddNoJSONEscape(pointer(ErrorMsg),length(ErrorMsg));
  36903. AddShort(#13#10'}');
  36904. end else begin
  36905. AddShort(','#13#10'"errorText":"');
  36906. AddJSONEscape(pointer(ErrorMsg));
  36907. AddShort('"'#13#10'}');
  36908. end;
  36909. SetText(Call.OutBody);
  36910. finally
  36911. Free;
  36912. end;
  36913. Server.InternalLog('%.Error: %',[ClassType,Call.OutBody],sllDebug);
  36914. end;
  36915. { TSQLRestRoutingREST }
  36916. procedure TSQLRestRoutingREST.URIDecodeSOAByInterface;
  36917. var i: integer;
  36918. method,clientdrivenid: RawUTF8;
  36919. begin
  36920. if (Table=nil) and (MethodIndex<0) and (URI<>'') and (Server.Services<>nil) then begin
  36921. // check URI as '/Model/Interface.Method[/ClientDrivenID]'
  36922. i := Server.Services.fListInterfaceMethods.FindHashed(URI);
  36923. if i>=0 then // no specific message: it may be a valid request
  36924. with Server.Services.fListInterfaceMethod[i] do begin
  36925. Service := TServiceFactoryServer(InterfaceService);
  36926. ServiceMethodIndex := InterfaceMethodIndex;
  36927. fServiceListInterfaceMethodIndex := i;
  36928. ServiceInstanceID := GetInteger(pointer(URIBlobFieldName));
  36929. end else
  36930. if URIBlobFieldName<>'' then begin
  36931. // check URI as '/Model/Interface/Method[/ClientDrivenID]''
  36932. i := Server.Services.fList.IndexOf(URI);
  36933. if i>=0 then begin // identified as a valid JSON-RPC service
  36934. Service := TServiceFactoryServer(Server.Services.fList.Objects[i]);
  36935. Split(URIBlobFieldName,'/',method,clientdrivenid);
  36936. ServiceMethodIndex := Service.InterfaceFactory.FindMethodIndex(method);
  36937. if ServiceMethodIndex<0 then
  36938. Service := nil else begin
  36939. inc(ServiceMethodIndex,SERVICE_PSEUDO_METHOD_COUNT);
  36940. fServiceListInterfaceMethodIndex := -1;
  36941. ServiceInstanceID := GetInteger(pointer(clientdrivenid));
  36942. end;
  36943. end;
  36944. end;
  36945. end;
  36946. end;
  36947. procedure TSQLRestRoutingREST.ExecuteSOAByInterface;
  36948. var JSON: RawUTF8;
  36949. Par: PUTF8Char;
  36950. meth,a,i,iLow: Integer;
  36951. WR: TTextWriter;
  36952. argDone: boolean;
  36953. begin // here Ctxt.Service and ServiceMethodIndex are set
  36954. if (Server.Services=nil) or (Service=nil) then
  36955. raise EServiceException.CreateUTF8('%.ExecuteSOAByInterface invalid call',[self]);
  36956. // URI as '/Model/Interface.Method[/ClientDrivenID]'
  36957. if Call.InBody<>'' then
  36958. // either parameters were sent as JSON array (the Delphi/AJAX way)
  36959. ServiceParameters := pointer(Call.InBody) else begin
  36960. // or parameters were URI-encoded (the HTML way)
  36961. Par := Parameters;
  36962. if Par<>nil then begin
  36963. while Par^='+' do inc(Par); // ignore trailing spaces
  36964. if (Par^='[') or IdemPChar(Par,'%5B') then
  36965. // either as JSON array (input is e.g. '+%5B...' for ' [...')
  36966. JSON := UrlDecode(Parameters) else begin
  36967. // or as a list of parameters (input is 'Param1=Value1&Param2=Value2...')
  36968. FillInput; // fInput[0]='Param1',fInput[1]='Value1',fInput[2]='Param2'...
  36969. if fInput<>nil then begin
  36970. meth := ServiceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT;
  36971. if cardinal(meth)<Service.InterfaceFactory.MethodsCount then begin
  36972. WR := TJSONSerializer.CreateOwnedStream;
  36973. try // convert URI parameters into the expected ordered JSON array
  36974. WR.Add('[');
  36975. with Service.InterfaceFactory.fMethods[meth] do begin
  36976. iLow := 0;
  36977. for a := ArgsInFirst to ArgsInLast do
  36978. with Args[a] do
  36979. if ValueDirection<>smdOut then begin
  36980. argDone := false;
  36981. for i := iLow to high(fInput)shr 1 do // search argument in URI
  36982. if IdemPropName(ParamName^,pointer(fInput[i*2]),length(fInput[i*2])) then begin
  36983. AddValueJSON(WR,fInput[i*2+1]); // will add "" if needed
  36984. if i=iLow then
  36985. inc(iLow); // optimistic in-order search, but allow any order
  36986. argDone := true;
  36987. break;
  36988. end;
  36989. if not argDone then
  36990. AddDefaultJSON(WR); // allow missing argument (and add ',')
  36991. end;
  36992. end;
  36993. WR.CancelLastComma;
  36994. WR.Add(']');
  36995. WR.SetText(JSON);
  36996. finally
  36997. WR.Free;
  36998. end;
  36999. end;
  37000. end;
  37001. end;
  37002. end;
  37003. ServiceParameters := pointer(JSON);
  37004. end;
  37005. // now Service, ServiceParameters, ServiceMethodIndex are set
  37006. InternalExecuteSOAByInterface;
  37007. end;
  37008. class procedure TSQLRestRoutingREST.ClientSideInvoke(var uri: RawUTF8;
  37009. const method, params, clientDrivenID: RawUTF8; out sent: RawUTF8);
  37010. begin
  37011. if clientDrivenID<>'' then
  37012. uri := uri+'.'+method+'/'+clientDrivenID else
  37013. uri := uri+'.'+method;
  37014. sent := '['+params+']'; // we may also encode them within the URI
  37015. end;
  37016. { TSQLRestRoutingJSON_RPC }
  37017. procedure TSQLRestRoutingJSON_RPC.URIDecodeSOAByInterface;
  37018. var i: integer;
  37019. begin
  37020. if (Table=nil) and (MethodIndex<0) and (URI<>'') and (Server.Services<>nil) then begin
  37021. // URI as '/Model/Interface'
  37022. i := Server.Services.fList.IndexOf(URI);
  37023. if i>=0 then // identified as a valid JSON-RPC service
  37024. Service := TServiceFactoryServer(Server.Services.fList.Objects[i]);
  37025. end; // ServiceMethodIndex will be retrieved from "method": in body
  37026. end;
  37027. procedure TSQLRestRoutingJSON_RPC.ExecuteSOAByInterface;
  37028. var method: RawUTF8;
  37029. Values: TPUtf8CharDynArray;
  37030. internal: TServiceInternalMethod;
  37031. tmp: TSynTempBuffer;
  37032. begin // here Ctxt.Service is set (not ServiceMethodIndex yet)
  37033. if (Server.Services=nil) or (Service=nil) then
  37034. raise EServiceException.CreateUTF8('%.ExecuteSOAByInterface invalid call',[self]);
  37035. tmp.Init(call.Inbody);
  37036. try
  37037. JSONDecode(tmp.buf,['method','params','id'],Values,True);
  37038. if Values[0]=nil then // Method name required
  37039. exit;
  37040. SetString(method,Values[0],StrLen(Values[0]));
  37041. ServiceParameters := Values[1];
  37042. ServiceInstanceID := GetCardinal(Values[2]); // retrieve "id":ClientDrivenID
  37043. ServiceMethodIndex := Service.fInterface.FindMethodIndex(method);
  37044. if ServiceMethodIndex>=0 then
  37045. inc(ServiceMethodIndex,SERVICE_PSEUDO_METHOD_COUNT) else begin
  37046. for internal := low(TServiceInternalMethod) to high(TServiceInternalMethod) do
  37047. if IdemPropNameU(method,SERVICE_PSEUDO_METHOD[internal]) then begin
  37048. ServiceMethodIndex := ord(internal);
  37049. break;
  37050. end;
  37051. if ServiceMethodIndex<0 then begin
  37052. Error('Unknown method');
  37053. exit;
  37054. end;
  37055. end;
  37056. // now Service, ServiceParameters, ServiceMethodIndex are set
  37057. InternalExecuteSOAByInterface;
  37058. finally
  37059. tmp.Done; // release temp storage for Values[] = Service* fields
  37060. end;
  37061. end;
  37062. class procedure TSQLRestRoutingJSON_RPC.ClientSideInvoke(var uri: RawUTF8;
  37063. const method, params, clientDrivenID: RawUTF8; out sent: RawUTF8);
  37064. begin
  37065. sent := '{"method":"'+method+'","params":['+params;
  37066. if clientDrivenID='' then
  37067. sent := sent+']}' else
  37068. sent := sent+'],"id":'+clientDrivenID+'}';
  37069. end;
  37070. function TSQLRestServer.ServiceRegister(
  37071. aImplementationClass: TInterfacedClass; const aInterfaces: array of PTypeInfo;
  37072. aInstanceCreation: TServiceInstanceImplementation;
  37073. const aContractExpected: RawUTF8): TServiceFactoryServer;
  37074. begin
  37075. if (aImplementationClass=nil) or (high(aInterfaces)<0) then
  37076. result := nil else
  37077. result := (ServiceContainer as TServiceContainerServer).
  37078. AddImplementation(aImplementationClass,aInterfaces,aInstanceCreation,nil,aContractExpected);
  37079. end;
  37080. function TSQLRestServer.ServiceRegister(aSharedImplementation: TInterfacedObject;
  37081. const aInterfaces: array of PTypeInfo; const aContractExpected: RawUTF8): TServiceFactoryServer;
  37082. begin
  37083. if (self=nil) or (aSharedImplementation=nil) or (high(aInterfaces)<0) then
  37084. result := nil else
  37085. result := (ServiceContainer as TServiceContainerServer).
  37086. AddImplementation(TInterfacedClass(aSharedImplementation.ClassType),
  37087. aInterfaces,sicShared,aSharedImplementation,aContractExpected);
  37088. end;
  37089. function TSQLRestServer.ServiceRegister(aClient: TSQLRest;
  37090. const aInterfaces: array of PTypeInfo;
  37091. aInstanceCreation: TServiceInstanceImplementation;
  37092. const aContractExpected: RawUTF8): boolean;
  37093. begin
  37094. result := False;
  37095. if (self=nil) or (high(aInterfaces)<0) or (aClient=nil) then
  37096. exit;
  37097. result := (ServiceContainer as TServiceContainerServer).AddInterface(
  37098. aInterfaces,aInstanceCreation,aContractExpected);
  37099. end;
  37100. function TSQLRestServer.ServiceDefine(aImplementationClass: TInterfacedClass;
  37101. const aInterfaces: array of TGUID; aInstanceCreation: TServiceInstanceImplementation;
  37102. const aContractExpected: RawUTF8): TServiceFactoryServer;
  37103. begin
  37104. result := ServiceRegister(aImplementationClass,
  37105. TInterfaceFactory.GUID2TypeInfo(aInterfaces),aInstanceCreation,aContractExpected);
  37106. end;
  37107. function TSQLRestServer.ServiceDefine(aSharedImplementation: TInterfacedObject;
  37108. const aInterfaces: array of TGUID; const aContractExpected: RawUTF8): TServiceFactoryServer;
  37109. begin
  37110. result := ServiceRegister(aSharedImplementation,
  37111. TInterfaceFactory.GUID2TypeInfo(aInterfaces),aContractExpected);
  37112. end;
  37113. function TSQLRestServer.ServiceDefine(aClient: TSQLRest;
  37114. const aInterfaces: array of TGUID;
  37115. aInstanceCreation: TServiceInstanceImplementation;
  37116. const aContractExpected: RawUTF8): boolean;
  37117. begin
  37118. result := ServiceRegister(aClient,
  37119. TInterfaceFactory.GUID2TypeInfo(aInterfaces),
  37120. aInstanceCreation,aContractExpected);
  37121. end;
  37122. procedure TSQLRestServer.URI(var Call: TSQLRestURIParams);
  37123. const COMMANDTEXT: array[TSQLRestServerURIContextCommand] of string[15] =
  37124. ('','SOA-Method ','SOA-Interface ','ORM-Get ','ORM-Write ');
  37125. var Ctxt: TSQLRestServerURIContext;
  37126. timeStart,timeEnd: Int64;
  37127. elapsed, len: cardinal;
  37128. outcomingfile: boolean;
  37129. {$ifdef WITHLOG}
  37130. Log: ISynLog; // for Enter auto-leave to work with FPC
  37131. begin
  37132. Log := fLogClass.Enter('URI(% % inlen=%)',[Call.Method,Call.Url,length(Call.InBody)],self);
  37133. {$else}
  37134. begin
  37135. {$endif}
  37136. QueryPerformanceCounter(timeStart);
  37137. fStats.AddCurrentRequestCount(1);
  37138. Call.OutInternalState := InternalState; // other threads may change it
  37139. Call.OutStatus := HTML_BADREQUEST; // default error code is 400 BAD REQUEST
  37140. Ctxt := ServicesRouting.Create(self,Call);
  37141. try
  37142. {$ifdef WITHLOG}
  37143. Ctxt.Log := Log.Instance;
  37144. {$endif}
  37145. if fShutdownRequested then
  37146. Ctxt.Error('Server is shutting down',HTML_UNAVAILABLE) else
  37147. if Ctxt.Method=mNone then
  37148. Ctxt.Error('Unknown VERB') else
  37149. // 1. decode URI
  37150. if not Ctxt.URIDecodeREST then
  37151. Ctxt.Error('Invalid Root',HTML_NOTFOUND) else
  37152. if (RootRedirectGet<>'') and (Ctxt.Method=mGet) and
  37153. (Call.Url=Model.Root) and (Call.InBody='') then
  37154. Ctxt.Redirect(RootRedirectGet) else begin
  37155. Ctxt.URIDecodeSOAByMethod;
  37156. if (Ctxt.MethodIndex<0) and (Ctxt.URI<>'') then
  37157. Ctxt.URIDecodeSOAByInterface;
  37158. // 2. handle security
  37159. if not Ctxt.Authenticate then
  37160. Ctxt.AuthenticationFailed(afInvalidSignature) else
  37161. if (Ctxt.Service<>nil) and
  37162. not (reService in Call.RestAccessRights^.AllowRemoteExecute) then
  37163. if (rsoRedirectForbiddenToAuth in Options) and (Ctxt.ClientKind=ckAjax) then
  37164. Ctxt.Redirect(Model.Root+'/auth') else
  37165. Ctxt.AuthenticationFailed(afRemoteServiceExecutionNotAllowed) else
  37166. // 3. call appropriate ORM / SOA commands in fAcquireExecution[] context
  37167. try
  37168. if Ctxt.MethodIndex>=0 then
  37169. if Ctxt.MethodIndex=fPublishedMethodBatchIndex then
  37170. Ctxt.Command := execORMWrite else
  37171. Ctxt.Command := execSOAByMethod else
  37172. if Ctxt.Service<>nil then
  37173. Ctxt.Command := execSOAByInterface else
  37174. if Ctxt.Method in [mLOCK,mGET,mUNLOCK,mSTATE] then
  37175. // handle read methods
  37176. Ctxt.Command := execORMGet else
  37177. // write methods (mPOST, mPUT, mDELETE...)
  37178. Ctxt.Command := execORMWrite;
  37179. if (not Assigned(OnBeforeURI)) or OnBeforeURI(Ctxt) then
  37180. Ctxt.ExecuteCommand;
  37181. except
  37182. on E: Exception do
  37183. if (not Assigned(OnErrorURI)) or OnErrorURI(Ctxt,E) then
  37184. // return 500 internal server error
  37185. Ctxt.Error(E,'',[],HTML_SERVERERROR);
  37186. end;
  37187. end;
  37188. // 4. returns expected result to the client and update Server statistics
  37189. if StatusCodeIsSuccess(Call.OutStatus) then begin
  37190. outcomingfile := false;
  37191. if Call.OutBody<>'' then begin
  37192. len := length(Call.OutHead);
  37193. outcomingfile := (len>=25) and (Call.OutHead[15]='!') and
  37194. IdemPChar(pointer(Call.OutHead),STATICFILE_CONTENT_TYPE_HEADER_UPPPER);
  37195. end else // Call.OutBody=''
  37196. if (Call.OutStatus=HTML_SUCCESS) and
  37197. (rsoHtml200WithNoBodyReturns204 in fOptions) then
  37198. Call.OutStatus := HTML_NOCONTENT;
  37199. fStats.ProcessSuccess(outcomingfile);
  37200. end else begin
  37201. fStats.ProcessErrorNumber(Call.OutStatus);
  37202. if Call.OutBody='' then // if no custom error message, compute it now as JSON
  37203. Ctxt.Error(Ctxt.CustomErrorMsg,Call.OutStatus);
  37204. end;
  37205. StatsAddSizeForCall(fStats,Call);
  37206. if (Ctxt.Static<>nil) and Ctxt.Static.InheritsFrom(TSQLRestStorage) and
  37207. TSQLRestStorage(Ctxt.Static).fOutInternalStateForcedRefresh then
  37208. // force always refresh for Static table which demands it
  37209. Call.OutInternalState := cardinal(-1) else
  37210. // database state may have changed above
  37211. Call.OutInternalState := InternalState;
  37212. if Ctxt.OutSetCookie<>'' then
  37213. Call.OutHead := Trim(Call.OutHead+#13#10'Set-Cookie: '+Ctxt.OutSetCookie+
  37214. '; Path=/'); // not Path=/ModelRoot, since would be case sensitive
  37215. finally
  37216. QueryPerformanceCounter(timeEnd);
  37217. Ctxt.MicroSecondsElapsed := fStats.FromExternalQueryPerformanceCounters(timeEnd-timeStart);
  37218. {$ifdef WITHLOG}
  37219. InternalLog('% % % %/% %-> % with outlen=% in % us',
  37220. [Ctxt.SessionUserName,Ctxt.SessionRemoteIP,Call.Method,Model.Root,Ctxt.URI,
  37221. COMMANDTEXT[Ctxt.Command],Call.OutStatus,length(Call.OutBody),Ctxt.MicroSecondsElapsed],sllServer);
  37222. if (Call.OutBody<>'') and (sllServiceReturn in fLogFamily.Level) then
  37223. if (Ctxt.ServiceExecution=nil) or not(optNoLogOutput in Ctxt.ServiceExecution^.Options) then
  37224. if IsHTMLContentTypeTextual(pointer(Call.OutHead)) then
  37225. fLogFamily.SynLog.Log(sllServiceReturn,Call.OutBody,self,MAX_SIZE_RESPONSE_LOG);
  37226. {$endif}
  37227. if mlTables in StatLevels then
  37228. case Ctxt.Command of
  37229. execORMGet:
  37230. fStats.NotifyORMTable(Ctxt.TableIndex,length(Call.OutBody),false,Ctxt.MicroSecondsElapsed);
  37231. execORMWrite:
  37232. fStats.NotifyORMTable(Ctxt.TableIndex,length(Call.InBody),true,Ctxt.MicroSecondsElapsed);
  37233. end;
  37234. fStats.AddCurrentRequestCount(-1);
  37235. if fStatUsage<>nil then
  37236. fStatUsage.Modified(fStats,[]);
  37237. if Assigned(OnAfterURI) then
  37238. try
  37239. OnAfterURI(Ctxt);
  37240. except
  37241. end;
  37242. Ctxt.Free;
  37243. end;
  37244. if Assigned(OnIdle) then begin
  37245. elapsed := GetTickCount64 shr 7; // trigger every 128 ms
  37246. if elapsed<>fOnIdleLastTix then begin
  37247. OnIdle(self);
  37248. fOnIdleLastTix := elapsed;
  37249. end;
  37250. end;
  37251. end;
  37252. function TSQLRestServer.FullStatsAsJson: RawUTF8;
  37253. var Ctxt: TSQLRestServerURIContext;
  37254. call: TSQLRestURIParams;
  37255. begin // emulates root/stat?withall=1 method call
  37256. Ctxt := TSQLRestRoutingREST.Create(Self,call);
  37257. try
  37258. Ctxt.Parameters := 'withall=1';
  37259. Stat(Ctxt);
  37260. result := Call.OutBody;
  37261. finally
  37262. Ctxt.Free;
  37263. end;
  37264. end;
  37265. function TSQLRestServer.FullStatsAsDocVariant: variant;
  37266. begin
  37267. _Json(FullStatsAsJson,result,JSON_OPTIONS_FAST);
  37268. end;
  37269. procedure TSQLRestServer.InternalInfo(var info: TDocVariantData);
  37270. begin // called by root/TimeStamp/info REST method
  37271. info.AddNameValuesToObject(['exe', ExeVersion.ProgramName,
  37272. 'version', ExeVersion.Version.Detailed, 'started', Stats.StartDate,
  37273. 'clients', Stats.ClientsCurrent, 'methods', Stats.ServiceMethod,
  37274. 'interfaces', Stats.ServiceInterface, 'total', Stats.TaskCount,
  37275. 'time', Stats.TotalTime.Text, 'host', ExeVersion.Host]);
  37276. with TSynMonitorMemory.Create do
  37277. try
  37278. info.AddNameValuesToObject(['memused', KB(AllocatedUsed.Bytes),
  37279. 'memfree', FormatUTF8('% / %',[PhysicalMemoryFree.Text,PhysicalMemoryTotal.Text])]);
  37280. finally
  37281. Free;
  37282. end;
  37283. end;
  37284. procedure TSQLRestServer.InternalStat(Ctxt: TSQLRestServerURIContext; W: TTextWriter);
  37285. const READWRITE: array[boolean] of string[9] = ('{"read":','{"write":');
  37286. var s,i: integer;
  37287. withall,rw: boolean;
  37288. begin
  37289. Stats.ComputeDetailsTo(W);
  37290. W.CancelLastChar('}');
  37291. if fCache<>nil then begin
  37292. W.AddShort(',"cachedMemoryBytes":');
  37293. W.AddU(fCache.CachedMemory); // will also flush outdated JSON
  37294. W.Add(',');
  37295. end;
  37296. withall := Ctxt.InputExists['withall'];
  37297. if withall or Ctxt.InputExists['withtables'] then begin
  37298. W.CancelLastComma;
  37299. W.AddShort(',"tables":[');
  37300. Stats.Lock; // thread-safe Stats.fPerTable[] access
  37301. try
  37302. for i := 0 to fModel.TablesMax do begin
  37303. W.Add('{"%":[',[fModel.TableProps[i].Props.SQLTableName]);
  37304. for rw := False to True do
  37305. if (i<Length(Stats.fPerTable[rw])) and
  37306. (Stats.fPerTable[rw,i]<>nil) and
  37307. (Stats.fPerTable[rw,i].TaskCount<>0) then begin
  37308. W.AddShort(READWRITE[rw]);
  37309. Stats.fPerTable[rw,i].ComputeDetailsTo(W);
  37310. W.Add('}',',');
  37311. end;
  37312. W.CancelLastComma;
  37313. W.AddShort(']},');
  37314. end;
  37315. finally
  37316. Stats.UnLock;
  37317. end;
  37318. W.CancelLastComma;
  37319. W.Add(']',',');
  37320. end;
  37321. if withall or Ctxt.InputExists['withmethods'] then begin
  37322. W.CancelLastComma;
  37323. W.AddShort(',"methods":[');
  37324. for i := 0 to high(fPublishedMethod) do
  37325. with fPublishedMethod[i] do
  37326. if (Stats<>nil) and (Stats.TaskCount<>0) then begin
  37327. W.Add('{"%":',[Name]);
  37328. Stats.ComputeDetailsTo(W);
  37329. W.Add('}',',');
  37330. end;
  37331. W.CancelLastComma;
  37332. W.Add(']',',');
  37333. end;
  37334. if withall or Ctxt.InputExists['withinterfaces'] then begin
  37335. W.CancelLastComma;
  37336. W.AddShort(',"interfaces":[');
  37337. for s := 0 to fServices.Count-1 do
  37338. with fServices.Index(s) as TServiceFactoryServer do
  37339. for i := 0 to fInterface.MethodsCount-1 do
  37340. if fStats[i]<>nil then begin
  37341. W.Add('{"%":',[fInterface.fMethods[i].InterfaceDotMethodName]);
  37342. fStats[i].ComputeDetailsTo(W);
  37343. W.Add('}',',');
  37344. end;
  37345. W.CancelLastComma;
  37346. W.Add(']',',');
  37347. end;
  37348. if (withall or Ctxt.InputExists['withsessions']) and
  37349. (fSessions<>nil) then begin
  37350. W.CancelLastComma;
  37351. W.AddShort(',"sessions":[');
  37352. fSessions.Safe.Lock;
  37353. try
  37354. for s := 0 to fSessions.Count-1 do begin
  37355. W.WriteObject(fSessions.List[s]);
  37356. W.CancelLastChar('}');
  37357. with TAuthSession(fSessions.List[s]) do begin
  37358. W.AddShort(',"methods":[');
  37359. for i := 0 to high(fMethods) do
  37360. if fMethods[i]<>nil then begin
  37361. W.Add('{"%":',[fPublishedMethod[i].Name]);
  37362. fMethods[i].ComputeDetailsTo(W);
  37363. W.Add('}',',');
  37364. end;
  37365. W.CancelLastComma;
  37366. W.AddShort('],"interfaces":[');
  37367. for i := 0 to high(fInterfaces) do
  37368. if fInterfaces[i]<>nil then begin
  37369. W.Add('{"%":',[Services.fListInterfaceMethod[i].InterfaceDotMethodName]);
  37370. fInterfaces[i].ComputeDetailsTo(W);
  37371. W.Add('}',',');
  37372. end;
  37373. W.CancelLastComma;
  37374. W.AddShort(']},');
  37375. end;
  37376. end;
  37377. finally
  37378. fSessions.Safe.UnLock;
  37379. end;
  37380. W.CancelLastComma;
  37381. W.Add(']',',');
  37382. end;
  37383. W.CancelLastComma;
  37384. W.Add('}');
  37385. end;
  37386. procedure TSQLRestServer.Stat(Ctxt: TSQLRestServerURIContext);
  37387. var W: TTextWriter;
  37388. json,xml,name: RawUTF8;
  37389. begin
  37390. W := TJSONSerializer.CreateOwnedStream;
  37391. try
  37392. name := Ctxt.InputUTF8OrVoid['findservice'];
  37393. if name='' then begin
  37394. InternalStat(Ctxt,W);
  37395. name := 'Stats';
  37396. end else
  37397. AssociatedServices.FindServiceAll(name,W);
  37398. W.SetText(json);
  37399. if Ctxt.InputExists['format'] or
  37400. IdemPropNameU(Ctxt.URIBlobFieldName,'json') then
  37401. json := JSONReformat(json) else
  37402. if IdemPropNameU(Ctxt.URIBlobFieldName,'xml') then begin
  37403. JSONBufferToXML(pointer(json),XMLUTF8_HEADER,'<'+name+'>',xml);
  37404. Ctxt.Returns(xml,200,XML_CONTENT_TYPE_HEADER);
  37405. exit;
  37406. end;
  37407. Ctxt.Returns(json);
  37408. finally
  37409. W.Free;
  37410. end;
  37411. end;
  37412. procedure TSQLRestServer.SetStatUsage(usage: TSynMonitorUsage);
  37413. begin
  37414. if fStatUsage=usage then
  37415. exit;
  37416. if usage=nil then begin
  37417. // e.g. from TTestServiceOrientedArchitecture.ClientSideRESTSessionsStats
  37418. FreeAndNil(fStatUsage);
  37419. exit;
  37420. end;
  37421. if fStatUsage<>nil then
  37422. raise EModelException.CreateUTF8('%.StatUsage should be set once', [self]);
  37423. fStatUsage := usage;
  37424. fStatUsage.Track(fStats,'rest');
  37425. end;
  37426. procedure TSQLRestServer.AdministrationExecute(const DatabaseName,SQL: RawUTF8;
  37427. var result: TServiceCustomAnswer);
  37428. var isAjax: boolean;
  37429. name,interf,method: RawUTF8;
  37430. obj: TObject;
  37431. call: TSQLRestURIParams;
  37432. info: TDocVariantData;
  37433. P: PUTF8Char;
  37434. procedure PrepareCall;
  37435. begin
  37436. call.Init;
  37437. BYPASS_ACCESS_RIGHTS := SUPERVISOR_ACCESS_RIGHTS;
  37438. call.RestAccessRights := @BYPASS_ACCESS_RIGHTS;
  37439. call.Url := Model.Root;
  37440. end;
  37441. begin
  37442. isAjax := not NoAjaxJson;
  37443. if isAjax then
  37444. NoAjaxJson := true; // reduce memory use from a Delphi (ToolsAdmin) tool
  37445. try
  37446. if (SQL<>'') and (SQL[1]='#') then begin
  37447. P := @SQL[2];
  37448. case IdemPCharArray(P,['INTERFACES','STATS(','STATS','SERVICES','SESSIONS',
  37449. 'GET','POST','WRAPPER','HELP','INFO']) of
  37450. 0: result.Content := ServicesPublishedInterfaces;
  37451. 1: begin
  37452. name := copy(SQL,8,length(SQL)-8);
  37453. obj := ServiceMethodStat[name];
  37454. if obj=nil then begin
  37455. Split(name,'.',interf,method);
  37456. obj := Services[interf];
  37457. if obj<>nil then
  37458. obj := (obj as TServiceFactoryServer).Stat[method] else
  37459. obj := nil;
  37460. end;
  37461. if obj<>nil then
  37462. result.Content := ObjectToJSON(obj);
  37463. end;
  37464. 2: result.Content := FullStatsAsJson;
  37465. 3: result.Content := Services.AsJson;
  37466. 4: result.Content := SessionsAsJson;
  37467. 5,6: begin
  37468. PrepareCall;
  37469. call.Method := GetNextItem(P,' '); // GET or POST
  37470. if P<>nil then
  37471. call.Url := call.Url+'/'+RawUTF8(P);
  37472. URI(call);
  37473. result.Content := call.OutBody;
  37474. end;
  37475. 7: begin
  37476. PrepareCall;
  37477. call.Method := 'GET';
  37478. call.Url := call.Url+'/wrapper/context';
  37479. URI(call);
  37480. result.Content := call.OutBody;
  37481. end;
  37482. 8: begin
  37483. inherited;
  37484. result.Content[length(result.Content)] := '|';
  37485. result.Content := result.Content+'#interfaces|#wrapper|#info|'+
  37486. '#stats|#stats(method)|#stats(interface.method)|#services|#sessions|'+
  37487. '#get url|#post url"';
  37488. end;
  37489. 9: begin
  37490. info.InitJSONInPlace(pointer(result.Content)); // from DatabaseExecute()
  37491. InternalInfo(info);
  37492. result.Content := info.ToJSON;
  37493. end;
  37494. else inherited AdministrationExecute(DatabaseName,SQL,result);
  37495. end;
  37496. end else
  37497. inherited; // will execute the SQL
  37498. finally
  37499. NoAjaxJson := not isAjax;
  37500. end;
  37501. end;
  37502. procedure TSQLRestServer.TimeStamp(Ctxt: TSQLRestServerURIContext);
  37503. var
  37504. info: TDocVariantData;
  37505. begin
  37506. if IdemPropNameU(Ctxt.URIBlobFieldName,'info') then begin
  37507. info.InitFast;
  37508. InternalInfo(info);
  37509. Ctxt.Returns(info.ToJSON('','',jsonHumanReadable));
  37510. end else
  37511. Ctxt.Returns(Int64ToUtf8(ServerTimeStamp),HTML_SUCCESS,TEXT_CONTENT_TYPE_HEADER);
  37512. end;
  37513. procedure TSQLRestServer.CacheFlush(Ctxt: TSQLRestServerURIContext);
  37514. begin
  37515. case Ctxt.Method of
  37516. mGET: begin
  37517. if Ctxt.Table=nil then
  37518. Cache.Flush else
  37519. if Ctxt.TableID=0 then
  37520. Cache.Flush(Ctxt.Table) else
  37521. Cache.SetCache(Ctxt.Table,Ctxt.TableID);
  37522. Ctxt.Success;
  37523. end;
  37524. mPOST:
  37525. if Ctxt.URIBlobFieldName='_callback_' then
  37526. (Services as TServiceContainerServer).FakeCallbackRelease(Ctxt);
  37527. end;
  37528. end;
  37529. procedure TSQLRestServer.Batch(Ctxt: TSQLRestServerURIContext);
  37530. var Results: TInt64DynArray;
  37531. i: integer;
  37532. begin
  37533. if not (Ctxt.Method in [mPUT,mPOST]) then begin
  37534. Ctxt.Error('PUT/POST only');
  37535. exit;
  37536. end;
  37537. try
  37538. EngineBatchSend(Ctxt.Table,Ctxt.Call.InBody,TIDDynArray(Results),0);
  37539. except
  37540. on E: Exception do begin
  37541. Ctxt.Error(E,'did break % BATCH process',[Ctxt.Table],HTML_SERVERERROR);
  37542. exit;
  37543. end;
  37544. end;
  37545. // send back operation status array
  37546. Ctxt.Call.OutStatus := HTML_SUCCESS;
  37547. for i := 0 to length(Results)-1 do
  37548. if Results[i]<>HTML_SUCCESS then begin
  37549. Ctxt.Call.OutBody := Int64DynArrayToCSV(Results,length(Results),'[',']');
  37550. exit;
  37551. end;
  37552. Ctxt.Call.OutBody := '["OK"]'; // to save bandwith if no adding
  37553. end;
  37554. function ServerNonce(Previous: boolean): RawUTF8;
  37555. var Ticks: cardinal;
  37556. begin
  37557. Ticks := GetTickCount64 div (1000*60*5); // valid for 5*60*1000 ms = 5 minutes
  37558. if Previous then
  37559. dec(Ticks);
  37560. result := SHA256(@Ticks,sizeof(Ticks)); // naive but sufficient nonce
  37561. end;
  37562. procedure TSQLRestServer.SessionCreate(var User: TSQLAuthUser;
  37563. Ctxt: TSQLRestServerURIContext; out Session: TAuthSession);
  37564. var i: integer;
  37565. begin
  37566. Session := nil;
  37567. if (reOneSessionPerUser in Ctxt.Call^.RestAccessRights^.AllowRemoteExecute) and
  37568. (fSessions<>nil) then
  37569. for i := 0 to fSessions.Count-1 do
  37570. if TAuthSession(fSessions.List[i]).User.fID=User.fID then begin
  37571. {$ifdef WITHLOG}
  37572. with TAuthSession(fSessions.List[i]) do
  37573. Ctxt.Log.Log(sllUserAuth,'User.LogonName=% already connected from "%/%"',
  37574. [User.LogonName,RemoteIP,Ctxt.Call^.LowLevelConnectionID],self);
  37575. {$endif}
  37576. Ctxt.AuthenticationFailed(afSessionAlreadyStartedForThisUser);
  37577. exit; // user already connected
  37578. end;
  37579. Session := fSessionClass.Create(Ctxt,User);
  37580. if Assigned(OnSessionCreate) then
  37581. if OnSessionCreate(self,Session,Ctxt) then begin // TRUE aborts session creation
  37582. {$ifdef WITHLOG}
  37583. Ctxt.Log.Log(sllUserAuth,'Session aborted by OnSessionCreate() callback '+
  37584. 'for User.LogonName=% (connected from "%/%") - clients=%, sessions=%',
  37585. [User.LogonName,Session.RemoteIP,Ctxt.Call^.LowLevelConnectionID,
  37586. fStats.GetClientsCurrent,fSessions.Count],self);
  37587. {$endif}
  37588. Ctxt.AuthenticationFailed(afSessionCreationAborted);
  37589. User := nil;
  37590. FreeAndNil(Session);
  37591. exit;
  37592. end;
  37593. User := nil; // will be freed by TAuthSession.Destroy
  37594. fSessions.Add(Session);
  37595. fStats.ClientConnect;
  37596. end;
  37597. procedure TSQLRestServer.Auth(Ctxt: TSQLRestServerURIContext);
  37598. var i: integer;
  37599. begin
  37600. if fSessionAuthentication=nil then
  37601. exit;
  37602. fSessions.Safe.Lock;
  37603. try
  37604. for i := 0 to length(fSessionAuthentication)-1 do
  37605. if fSessionAuthentication[i].Auth(Ctxt) then
  37606. break; // found an authentication, which may be successfull or not
  37607. finally
  37608. fSessions.Safe.UnLock;
  37609. end;
  37610. end;
  37611. procedure TSQLRestServer.SessionDelete(aSessionIndex: integer;
  37612. Ctxt: TSQLRestServerURIContext);
  37613. begin
  37614. if (self<>nil) and (cardinal(aSessionIndex)<cardinal(fSessions.Count)) then
  37615. with TAuthSession(fSessions.List[aSessionIndex]) do begin
  37616. if Services is TServiceContainerServer then
  37617. TServiceContainerServer(Services).OnCloseSession(IDCardinal);
  37618. if Ctxt=nil then
  37619. InternalLog('Deleted session %:%/%',
  37620. [User.LogonName,IDCardinal,fSessions.Count],sllUserAuth) else
  37621. InternalLog('Deleted session %:%/% from %/%',
  37622. [User.LogonName,IDCardinal,fSessions.Count,RemoteIP,Ctxt.Call^.LowLevelConnectionID],sllUserAuth);
  37623. if Assigned(OnSessionClosed) then
  37624. OnSessionClosed(self,fSessions.List[aSessionIndex],Ctxt);
  37625. fSessions.Delete(aSessionIndex);
  37626. fStats.ClientDisconnect;
  37627. end;
  37628. end;
  37629. function TSQLRestServer.SessionAccess(Ctxt: TSQLRestServerURIContext): TAuthSession;
  37630. var i: integer;
  37631. Tix64: Int64;
  37632. begin // caller shall be locked via fSessions.Safe.Lock
  37633. if (self<>nil) and (fSessions<>nil) then begin
  37634. // first check for outdated sessions to be deleted
  37635. Tix64 := GetTickCount64;
  37636. for i := fSessions.Count-1 downto 0 do
  37637. with TAuthSession(fSessions.List[i]) do
  37638. if Tix64>LastAccess64+TimeOutMS then
  37639. SessionDelete(i,nil);
  37640. // retrieve session
  37641. for i := 0 to fSessions.Count-1 do begin
  37642. result := TAuthSession(fSessions.List[i]);
  37643. if result.IDCardinal=Ctxt.Session then begin
  37644. result.fLastAccess64 := Tix64; // refresh session access timestamp
  37645. Ctxt.fAuthSession := result;
  37646. Ctxt.SessionUser := result.User.fID;
  37647. Ctxt.SessionGroup := result.User.GroupRights.fID;
  37648. Ctxt.SessionUserName := result.User.LogonName;
  37649. Ctxt.SessionRemoteIP := result.RemoteIP;
  37650. exit;
  37651. end;
  37652. end;
  37653. end;
  37654. result := nil;
  37655. end;
  37656. function TSQLRestServer.SessionGetUser(aSessionID: Cardinal): TSQLAuthUser;
  37657. var i: integer;
  37658. begin
  37659. result := nil;
  37660. if self=nil then
  37661. exit;
  37662. fSessions.Safe.Lock;
  37663. try
  37664. for i := 0 to fSessions.Count-1 do
  37665. with TAuthSession(fSessions.List[i]) do
  37666. if IDCardinal=aSessionID then begin
  37667. if User<>nil then begin
  37668. result := User.CreateCopy as fSQLAuthUserClass;
  37669. result.GroupRights := nil;
  37670. end;
  37671. Break;
  37672. end;
  37673. finally
  37674. fSessions.Safe.UnLock;
  37675. end;
  37676. end;
  37677. function TSQLRestServer.SessionsAsJson: RawJSON;
  37678. var i: integer;
  37679. begin
  37680. result := '';
  37681. if (self=nil) or (fSessions.Count=0) then
  37682. exit;
  37683. fSessions.Safe.Lock;
  37684. with TJSONSerializer.CreateOwnedStream do
  37685. try
  37686. Add('[');
  37687. for i := 0 to fSessions.Count-1 do begin
  37688. WriteObject(fSessions.List[i]);
  37689. Add(',');
  37690. end;
  37691. CancelLastComma;
  37692. Add(']');
  37693. SetText(RawUTF8(result));
  37694. finally
  37695. fSessions.Safe.UnLock;
  37696. Free;
  37697. end;
  37698. end;
  37699. const
  37700. MAGIC_SESSION: cardinal = $A5ABA5AB;
  37701. procedure TSQLRestServer.SessionsSaveToFile(const aFileName: TFileName);
  37702. var i: integer;
  37703. MS: TRawByteStringStream;
  37704. W: TFileBufferWriter;
  37705. s: RawByteString;
  37706. begin
  37707. if self=nil then
  37708. exit;
  37709. DeleteFile(aFileName);
  37710. MS := TRawByteStringStream.Create;
  37711. try
  37712. W := TFileBufferWriter.Create(MS);
  37713. fSessions.Safe.Lock;
  37714. try
  37715. W.WriteVarUInt32(InternalState);
  37716. SQLAuthUserClass.RecordProps.SaveBinaryHeader(W);
  37717. SQLAuthGroupClass.RecordProps.SaveBinaryHeader(W);
  37718. W.WriteVarUInt32(fSessions.Count);
  37719. for i := 0 to fSessions.Count-1 do
  37720. TAuthSession(fSessions.List[i]).SaveTo(W);
  37721. W.Write4(MAGIC_SESSION);
  37722. W.Flush;
  37723. finally
  37724. fSessions.Safe.UnLock;
  37725. W.Free;
  37726. end;
  37727. s := SynLZCompress(MS.DataString);
  37728. SymmetricEncrypt(MAGIC_SESSION,s);
  37729. FileFromString(s,aFileName,true);
  37730. finally
  37731. MS.Free;
  37732. end;
  37733. end;
  37734. procedure TSQLRestServer.SessionsLoadFromFile(const aFileName: TFileName;
  37735. andDeleteExistingFileAfterRead: boolean);
  37736. procedure ContentError;
  37737. begin
  37738. raise ESynException.CreateUTF8('%.SessionsLoadFromFile("%")',[self,aFileName]);
  37739. end;
  37740. var i,n: integer;
  37741. s: RawByteString;
  37742. R: TFileBufferReader;
  37743. P: PAnsiChar;
  37744. begin
  37745. if self=nil then
  37746. exit;
  37747. s := StringFromFile(aFileName);
  37748. SymmetricEncrypt(MAGIC_SESSION,s);
  37749. s := SynLZDecompress(s);
  37750. if s='' then
  37751. exit;
  37752. R.OpenFrom(pointer(s),length(s));
  37753. fSessions.Safe.Lock;
  37754. try
  37755. InternalState := R.ReadVarUInt32;
  37756. if not SQLAuthUserClass.RecordProps.CheckBinaryHeader(R) or
  37757. not SQLAuthGroupClass.RecordProps.CheckBinaryHeader(R) then
  37758. ContentError;
  37759. n := R.ReadVarUInt32;
  37760. P := R.CurrentMemory;
  37761. fSessions.Clear;
  37762. for i := 1 to n do begin
  37763. fSessions.Add(fSessionClass.CreateFrom(P,self));
  37764. fStats.ClientConnect;
  37765. end;
  37766. if PCardinal(P)^<>MAGIC_SESSION then
  37767. ContentError;
  37768. finally
  37769. fSessions.Safe.UnLock;
  37770. R.Close;
  37771. end;
  37772. if andDeleteExistingFileAfterRead then
  37773. DeleteFile(aFileName);
  37774. end;
  37775. function TSQLRestServer.CacheWorthItForTable(aTableIndex: cardinal): boolean;
  37776. begin
  37777. if self=nil then
  37778. result := false else
  37779. result := (aTableIndex>=cardinal(length(fStaticData))) or
  37780. (not fStaticData[aTableIndex].InheritsFrom(TSQLRestStorageInMemory));
  37781. end;
  37782. procedure TSQLRestServer.BeginCurrentThread(Sender: TThread);
  37783. var i, tc: integer;
  37784. CurrentThreadId: TThreadID;
  37785. begin
  37786. tc := fStats.NotifyThreadCount(1);
  37787. CurrentThreadId := GetCurrentThreadId;
  37788. if Sender=nil then
  37789. raise ECommunicationException.CreateUTF8('%.BeginCurrentThread(nil)',[self]);
  37790. InternalLog('BeginCurrentThread(%) root=% ThreadID=% ThreadCount=%',
  37791. [Sender.ClassType,Model.Root,pointer(CurrentThreadId),tc],sllTrace);
  37792. if Sender.ThreadID<>CurrentThreadId then
  37793. raise ECommunicationException.CreateUTF8(
  37794. '%.BeginCurrentThread(Thread.ID=%) and CurrentThreadID=% should match',
  37795. [self,Sender.ThreadID,CurrentThreadId]);
  37796. with PServiceRunningContext(@ServiceContext)^ do // P..(@..)^ for ONE GetTls()
  37797. if RunningThread<>Sender then // e.g. if length(TSQLHttpServer.fDBServers)>1
  37798. if RunningThread<>nil then
  37799. raise ECommunicationException.CreateUTF8('%.BeginCurrentThread() twice',[self]) else
  37800. RunningThread := Sender;
  37801. if fStaticVirtualTable<>nil then
  37802. for i := 0 to high(fStaticVirtualTable) do
  37803. if (fStaticVirtualTable[i]<>nil) and
  37804. fStaticVirtualTable[i].InheritsFrom(TSQLRestStorage) then
  37805. TSQLRestStorage(fStaticVirtualTable[i]).BeginCurrentThread(Sender);
  37806. end;
  37807. procedure TSQLRestServer.EndCurrentThread(Sender: TThread);
  37808. var i, tc: integer;
  37809. CurrentThreadId: TThreadID;
  37810. Inst: TServiceFactoryServerInstance;
  37811. begin
  37812. tc := fStats.NotifyThreadCount(-1);
  37813. CurrentThreadId := GetCurrentThreadId;
  37814. if Sender=nil then
  37815. raise ECommunicationException.CreateUTF8('%.EndCurrentThread(nil)',[self]);
  37816. InternalLog('EndCurrentThread(%) ThreadID=% ThreadCount=%',
  37817. [Sender.ClassType,pointer(CurrentThreadId),tc],sllTrace);
  37818. if Sender.ThreadID<>CurrentThreadId then
  37819. raise ECommunicationException.CreateUTF8(
  37820. '%.EndCurrentThread(%.ID=%) should match CurrentThreadID=%',
  37821. [self,Sender,Sender.ThreadID,CurrentThreadId]);
  37822. if fStaticVirtualTable<>nil then
  37823. for i := 0 to high(fStaticVirtualTable) do
  37824. if (fStaticVirtualTable[i]<>nil) and
  37825. fStaticVirtualTable[i].InheritsFrom(TSQLRestStorage) then
  37826. TSQLRestStorage(fStaticVirtualTable[i]).EndCurrentThread(Sender);
  37827. if Services<>nil then begin
  37828. Inst.InstanceID := PtrUInt(CurrentThreadId);
  37829. for i := 0 to Services.Count-1 do
  37830. with TServiceFactoryServer(Services.fList.Objects[i]) do
  37831. if InstanceCreation=sicPerThread then
  37832. InternalInstanceRetrieve(Inst,SERVICE_METHODINDEX_FREEINSTANCE);
  37833. end;
  37834. with PServiceRunningContext(@ServiceContext)^ do // P..(@..)^ for ONE GetTls()
  37835. if RunningThread<>nil then // e.g. if length(TSQLHttpServer.fDBServers)>1
  37836. if RunningThread<>Sender then
  37837. raise ECommunicationException.CreateUTF8(
  37838. '%.EndCurrentThread(%) should match RunningThread=%',
  37839. [self,Sender,RunningThread]) else
  37840. RunningThread := nil;
  37841. inherited EndCurrentThread(Sender); // should be done eventually
  37842. end;
  37843. { TSQLRecordModification }
  37844. function TSQLRecordModification.ModifiedID: TID;
  37845. begin
  37846. if self=nil then
  37847. result := 0 else
  37848. result := RecordRef(fModifiedRecord).ID;
  37849. end;
  37850. function TSQLRecordModification.ModifiedTable(Model: TSQLModel): TSQLRecordClass;
  37851. begin
  37852. if (self=nil) or (Model=nil) then
  37853. result := nil else
  37854. result := RecordRef(fModifiedRecord).Table(Model);
  37855. end;
  37856. function TSQLRecordModification.ModifiedTableIndex: integer;
  37857. begin
  37858. if self=nil then
  37859. result := 0 else
  37860. result := RecordRef(fModifiedRecord).TableIndex;
  37861. end;
  37862. { TSQLRecordHistory }
  37863. class procedure TSQLRecordHistory.InitializeTable(Server: TSQLRestServer;
  37864. const FieldName: RawUTF8; Options: TSQLInitializeTableOptions);
  37865. begin
  37866. inherited InitializeTable(Server,FieldName,Options);
  37867. if FieldName='' then
  37868. Server.CreateSQLMultiIndex(Self,['ModifiedRecord','Event'],false);
  37869. end;
  37870. destructor TSQLRecordHistory.Destroy;
  37871. begin
  37872. inherited;
  37873. fHistoryAdd.Free;
  37874. end;
  37875. constructor TSQLRecordHistory.CreateHistory(aClient: TSQLRest;
  37876. aTable: TSQLRecordClass; aID: TID);
  37877. var Reference: RecordRef;
  37878. Rec: TSQLRecord;
  37879. HistJson: TSQLRecordHistory;
  37880. begin
  37881. if (aClient=nil) or (aID<=0) then
  37882. raise EORMException.CreateUTF8('Invalid %.CreateHistory(%,%,%) call',
  37883. [self,aClient,aTable,aID]);
  37884. // read BLOB changes
  37885. Reference.From(aClient.Model,aTable,aID);
  37886. fModifiedRecord := Reference.Value;
  37887. fEvent := heArchiveBlob;
  37888. Create(aClient,'ModifiedRecord=? and Event=%',[ord(heArchiveBlob)],[fModifiedRecord]);
  37889. if fID<>0 then
  37890. aClient.RetrieveBlobFields(self); // load former fHistory field
  37891. if not HistoryOpen(aClient.Model) then
  37892. raise EORMException.CreateUTF8('HistoryOpen in %.CreateHistory(%,%,%)',
  37893. [self,aClient,aTable,aID]);
  37894. // append JSON changes
  37895. HistJson := RecordClass.CreateAndFillPrepare(aClient,
  37896. 'ModifiedRecord=? and Event<>%',[ord(heArchiveBlob)],[fModifiedRecord])
  37897. as TSQLRecordHistory;
  37898. try
  37899. if HistJson.FillTable.RowCount=0 then
  37900. exit; // no JSON to append
  37901. Rec := HistoryGetLast;
  37902. try
  37903. while HistJson.FillOne do begin
  37904. Rec.FillFrom(pointer(HistJson.SentDataJSON));
  37905. HistoryAdd(Rec,HistJson);
  37906. end;
  37907. HistorySave(nil); // update internal fHistory field
  37908. finally
  37909. Rec.Free;
  37910. end;
  37911. finally
  37912. HistJson.Free;
  37913. end;
  37914. // prepare for HistoryCount and HistoryGet() from internal fHistory field
  37915. HistoryOpen(aClient.Model);
  37916. end;
  37917. function TSQLRecordHistory.HistoryOpen(Model: TSQLModel): boolean;
  37918. var len: cardinal;
  37919. start,i: integer;
  37920. R: TFileBufferReader;
  37921. tmp: RawByteString;
  37922. begin
  37923. result := false;
  37924. fHistoryModel := Model;
  37925. fHistoryUncompressed := '';
  37926. fHistoryTable := ModifiedTable(Model);
  37927. fHistoryUncompressedCount := 0;
  37928. fHistoryUncompressedOffset := nil;
  37929. if fHistoryTable=nil then
  37930. exit; // invalid Model or ModifiedRecord
  37931. tmp := SynLZDecompress(fHistory);
  37932. len := length(tmp);
  37933. if len>4 then begin
  37934. R.OpenFrom(pointer(tmp),len);
  37935. if not fHistoryTable.RecordProps.CheckBinaryHeader(R) then
  37936. exit; // invalid content: TSQLRecord layout may have changed
  37937. R.ReadVarUInt32Array(fHistoryUncompressedOffset);
  37938. fHistoryUncompressedCount := length(fHistoryUncompressedOffset);
  37939. start := R.CurrentPosition;
  37940. for i := 0 to fHistoryUncompressedCount-1 do
  37941. inc(fHistoryUncompressedOffset[i],start);
  37942. fHistoryUncompressed := tmp;
  37943. end;
  37944. result := true;
  37945. end;
  37946. function TSQLRecordHistory.HistoryCount: integer;
  37947. begin
  37948. if (self=nil) or (fHistoryUncompressed='') then
  37949. result := 0 else
  37950. result := fHistoryUncompressedCount;
  37951. end;
  37952. function TSQLRecordHistory.HistoryGet(Index: integer;
  37953. out Event: TSQLHistoryEvent; out TimeStamp: TModTime; Rec: TSQLRecord): boolean;
  37954. var P: PAnsiChar;
  37955. begin
  37956. if cardinal(Index)>=cardinal(HistoryCount) then
  37957. result := false else begin
  37958. P := pointer(fHistoryUncompressed);
  37959. inc(P,fHistoryUncompressedOffset[Index]);
  37960. Event := TSQLHistoryEvent(P^); inc(P);
  37961. TimeStamp := FromVarUInt64(PByte(P));
  37962. if (Rec<>nil) and (Rec.RecordClass=fHistoryTable) then begin
  37963. if Event=heDelete then
  37964. Rec.ClearProperties else
  37965. Rec.SetBinaryValuesSimpleFields(P);
  37966. Rec.fID := ModifiedID;
  37967. end;
  37968. result := true;
  37969. end;
  37970. end;
  37971. function TSQLRecordHistory.HistoryGet(Index: integer; Rec: TSQLRecord): boolean;
  37972. var Event: TSQLHistoryEvent;
  37973. TimeStamp: TModTime;
  37974. begin
  37975. result := HistoryGet(Index,Event,TimeStamp,Rec);
  37976. end;
  37977. function TSQLRecordHistory.HistoryGet(Index: integer): TSQLRecord;
  37978. var Event: TSQLHistoryEvent;
  37979. TimeStamp: TModTime;
  37980. begin
  37981. if fHistoryTable=nil then
  37982. result := nil else begin
  37983. result := fHistoryTable.Create;
  37984. if not HistoryGet(Index,Event,TimeStamp,result) then
  37985. FreeAndNil(result);
  37986. end;
  37987. end;
  37988. function TSQLRecordHistory.HistoryGetLast(Rec: TSQLRecord): boolean;
  37989. begin
  37990. result := HistoryGet(fHistoryUncompressedCount-1,Rec);
  37991. end;
  37992. function TSQLRecordHistory.HistoryGetLast: TSQLRecord;
  37993. var Event: TSQLHistoryEvent;
  37994. TimeStamp: TModTime;
  37995. begin
  37996. if fHistoryTable=nil then
  37997. result := nil else begin
  37998. result := fHistoryTable.Create; // always return an instance
  37999. HistoryGet(fHistoryUncompressedCount-1,Event,TimeStamp,result);
  38000. end;
  38001. end;
  38002. procedure TSQLRecordHistory.HistoryAdd(Rec: TSQLRecord; Hist: TSQLRecordHistory);
  38003. begin
  38004. if (self=nil) or (fHistoryModel=nil) or (Rec.RecordClass<>fHistoryTable) then
  38005. exit;
  38006. if fHistoryAdd=nil then
  38007. fHistoryAdd := TFileBufferWriter.Create(TRawByteStringStream);
  38008. AddInteger(fHistoryAddOffset,fHistoryAddCount,fHistoryAdd.TotalWritten);
  38009. fHistoryAdd.Write1(Ord(Hist.Event));
  38010. fHistoryAdd.WriteVarUInt64(Hist.TimeStamp);
  38011. if Hist.Event<>heDelete then
  38012. Rec.GetBinaryValuesSimpleFields(fHistoryAdd);
  38013. end;
  38014. function TSQLRecordHistory.HistorySave(Server: TSQLRestServer;
  38015. LastRec: TSQLRecord): boolean;
  38016. var size,i,maxSize,TableHistoryIndex: integer;
  38017. firstOldIndex,firstOldOffset, firstNewIndex,firstNewOffset: integer;
  38018. newOffset: TIntegerDynArray;
  38019. DBRec: TSQLRecord;
  38020. HistTemp: TSQLRecordHistory;
  38021. W: TFileBufferWriter;
  38022. begin
  38023. result := false;
  38024. if (self=nil) or (fHistoryTable=nil) or (fModifiedRecord=0) then
  38025. exit; // wrong call
  38026. try
  38027. // ensure latest item matches "official" one, as read from DB
  38028. if (Server<>nil) and (LastRec<>nil) and (LastRec.fID=ModifiedID) then begin
  38029. DBRec := Server.Retrieve(ModifiedRecord);
  38030. if DBRec<>nil then
  38031. try // may be just deleted
  38032. if not DBRec.SameRecord(LastRec) then begin
  38033. HistTemp := RecordClass.Create as TSQLRecordHistory;
  38034. try
  38035. HistTemp.fEvent := heUpdate;
  38036. HistTemp.fTimeStamp := Server.ServerTimeStamp;
  38037. HistoryAdd(DBRec,HistTemp);
  38038. finally
  38039. HistTemp.Free;
  38040. end;
  38041. end;
  38042. finally
  38043. DBRec.Free;
  38044. end;
  38045. end;
  38046. if fHistoryAdd=nil then
  38047. exit; // nothing new
  38048. // ensure resulting size matches specified criteria
  38049. firstOldIndex := 0;
  38050. TableHistoryIndex := 0;
  38051. if Server=nil then
  38052. maxSize := maxInt else begin
  38053. TableHistoryIndex := Server.Model.GetTableIndexExisting(RecordClass);
  38054. maxSize := Server.fTrackChangesHistory[TableHistoryIndex].MaxUncompressedBlobSize;
  38055. end;
  38056. size := fHistoryAdd.TotalWritten;
  38057. if (size>maxSize) or (fHistoryUncompressedCount=0) then
  38058. // e.g. if fHistory.Add() is already bigger than expected
  38059. firstOldIndex := fHistoryUncompressedCount else begin
  38060. inc(size,Length(fHistoryUncompressed)-fHistoryUncompressedOffset[0]);
  38061. while (firstOldIndex<fHistoryUncompressedCount-1) and (size>maxSize) do begin
  38062. dec(size,fHistoryUncompressedOffset[firstOldIndex+1]-fHistoryUncompressedOffset[firstOldIndex]);
  38063. inc(firstOldIndex);
  38064. end;
  38065. end;
  38066. // creates and store new History BLOB
  38067. W := TFileBufferWriter.Create(TRawByteStringStream);
  38068. try
  38069. // compute offsets
  38070. if firstOldIndex=fHistoryUncompressedCount then
  38071. firstOldOffset := length(fHistoryUncompressed) else
  38072. firstOldOffset := fHistoryUncompressedOffset[firstOldIndex];
  38073. SetLength(newOffset,fHistoryUncompressedCount-firstOldIndex+fHistoryAddCount);
  38074. for i := firstOldIndex to fHistoryUncompressedCount-1 do
  38075. newOffset[i-firstOldIndex] := fHistoryUncompressedOffset[i]-firstOldOffset;
  38076. firstNewIndex := fHistoryUncompressedCount-firstOldIndex;
  38077. firstNewOffset := Length(fHistoryUncompressed)-firstOldOffset;
  38078. for i := 0 to fHistoryAddCount-1 do
  38079. newOffset[firstNewIndex+i] := fHistoryAddOffset[i]+firstNewOffset;
  38080. // write header
  38081. fHistoryTable.RecordProps.SaveBinaryHeader(W);
  38082. W.WriteVarUInt32Array(newOffset,length(newOffset),wkOffsetU);
  38083. // write data
  38084. W.Write(@PByteArray(fHistoryUncompressed)[firstOldOffset],firstNewOffset);
  38085. fHistoryAdd.Flush;
  38086. W.WriteBinary((fHistoryAdd.Stream as TRawByteStringStream).DataString);
  38087. W.Flush;
  38088. fHistoryUncompressed := (W.Stream as TRawByteStringStream).DataString;
  38089. fHistory := SynLZCompress(fHistoryUncompressed);
  38090. if (Server<>nil) and (fID<>0) then begin
  38091. Server.EngineUpdateField(TableHistoryIndex,
  38092. 'TimeStamp',Int64ToUTF8(Server.ServerTimeStamp),'RowID',Int64ToUtf8(fID));
  38093. Server.EngineUpdateBlob(TableHistoryIndex,fID,
  38094. RecordProps.BlobFields[0].PropInfo,fHistory);
  38095. end;
  38096. result := true;
  38097. finally
  38098. W.Free;
  38099. end;
  38100. finally
  38101. fHistoryUncompressed := '';
  38102. fHistoryUncompressedOffset := nil;
  38103. FreeAndNil(fHistoryAdd);
  38104. fHistoryAddOffset := nil;
  38105. fHistoryAddCount := 0;
  38106. end;
  38107. end;
  38108. procedure TSQLRestServer.TrackChangesFlush(aTableHistory: TSQLRecordHistoryClass);
  38109. var HistBlob: TSQLRecordHistory;
  38110. Rec: TSQLRecord;
  38111. HistJson: TSQLRecordHistory;
  38112. WhereClause, JSON: RawUTF8;
  38113. HistID, ModifiedRecord: TInt64DynArray;
  38114. TableHistoryIndex,i,HistIDCount,n: integer;
  38115. ModifRecord, ModifRecordCount, MaxRevisionJSON: integer;
  38116. {$ifdef WITHLOG}
  38117. Log: ISynLog; // for Enter auto-leave to work with FPC
  38118. begin
  38119. Log := fLogClass.Enter('TrackChangesFlush(%)',[aTableHistory],self);
  38120. {$else}
  38121. begin
  38122. {$endif}
  38123. fAcquireExecution[execORMWrite].Safe.Lock; // avoid race condition
  38124. try // low-level Add(TSQLRecordHistory) without cache
  38125. TableHistoryIndex := Model.GetTableIndexExisting(aTableHistory);
  38126. MaxRevisionJSON := fTrackChangesHistory[TableHistoryIndex].MaxRevisionJSON;
  38127. if MaxRevisionJSON<=0 then
  38128. MaxRevisionJSON := 10;
  38129. // we will compress into BLOB only when we got more than 10 revisions of a record
  38130. with MultiFieldValues(aTableHistory,'RowID,ModifiedRecord',
  38131. 'Event<>%',[ord(heArchiveBlob)],[]) do
  38132. try
  38133. GetRowValues(fFieldIndexID,HistID);
  38134. GetRowValues(FieldIndex('ModifiedRecord'),ModifiedRecord);
  38135. finally
  38136. Free;
  38137. end;
  38138. QuickSortInt64(pointer(ModifiedRecord),pointer(HistID),0,high(ModifiedRecord));
  38139. ModifRecord := 0;
  38140. ModifRecordCount := 0;
  38141. n := 0;
  38142. HistIDCount := 0;
  38143. for i := 0 to high(ModifiedRecord) do begin
  38144. if (ModifiedRecord[i]=0) or (HistID[i]=0) then
  38145. raise EORMException.CreateUTF8('%.TrackChangesFlush: Invalid %.ID=%',
  38146. [self,aTableHistory,HistID[i]]);
  38147. if ModifiedRecord[i]<>ModifRecord then begin
  38148. if ModifRecordCount>MaxRevisionJSON then
  38149. HistIDCount := n else
  38150. n := HistIDCount;
  38151. ModifRecord := ModifiedRecord[i];
  38152. ModifRecordCount := 1;
  38153. end else
  38154. inc(ModifRecordCount);
  38155. HistID[n] := HistID[i];
  38156. inc(n);
  38157. end;
  38158. if ModifRecordCount>MaxRevisionJSON then
  38159. HistIDCount := n;
  38160. if HistIDCount=0 then
  38161. exit; // nothing to compress
  38162. QuickSortInt64(Pointer(HistID),0,HistIDCount-1);
  38163. WhereClause := Int64DynArrayToCSV(HistID,HistIDCount,'RowID in (',')');
  38164. { following SQL is much slower with external tables, and won't work
  38165. with TSQLRestStorageInMemory -> manual process instead
  38166. WhereClause := FormatUTF8('ModifiedRecord in (select ModifiedRecord from '+
  38167. '(select ModifiedRecord, count(*) NumItems from % group by ModifiedRecord) '+
  38168. 'where NumItems>% order by ModifiedRecord) and History is null',
  38169. [aTableHistory.SQLTableName,MaxRevisionJSON]); }
  38170. Rec := nil;
  38171. HistBlob := nil;
  38172. HistJson := aTableHistory.CreateAndFillPrepare(self,WhereClause);
  38173. try
  38174. HistBlob := aTableHistory.Create;
  38175. while HistJson.FillOne do begin
  38176. if HistJson.ModifiedRecord<>HistBlob.ModifiedRecord then begin
  38177. if HistBlob.ModifiedRecord<>0 then
  38178. HistBlob.HistorySave(self,Rec);
  38179. FreeAndNil(Rec);
  38180. HistBlob.fHistory := '';
  38181. HistBlob.fID := 0;
  38182. HistBlob.fEvent := heArchiveBlob;
  38183. if not Retrieve('ModifiedRecord=? and Event=%',
  38184. [ord(heArchiveBlob)],[HistJson.ModifiedRecord],HistBlob) then
  38185. HistBlob.fModifiedRecord := HistJson.ModifiedRecord else
  38186. RetrieveBlobFields(HistBlob);
  38187. if not HistBlob.HistoryOpen(Model) then begin
  38188. InternalLog('Invalid %.History BLOB content for ID=%: % '+
  38189. 'layout may have changed -> flush any previous content',
  38190. [HistBlob.RecordClass,HistBlob.fID,HistJson.ModifiedTable(Model)],sllError);
  38191. HistBlob.fID := 0;
  38192. end;
  38193. if HistBlob.fID<>0 then // allow changes appending to HistBlob
  38194. Rec := HistBlob.HistoryGetLast else begin
  38195. // HistBlob.fID=0 -> no previous BLOB content
  38196. JSON := JSONEncode(['ModifiedRecord',HistJson.ModifiedRecord,
  38197. 'TimeStamp',ServerTimeStamp,'Event',ord(heArchiveBlob)]);
  38198. if HistJson.Event=heAdd then begin // allow versioning from scratch
  38199. HistBlob.fID := EngineAdd(TableHistoryIndex,JSON);
  38200. Rec := HistJson.ModifiedTable(Model).Create;
  38201. HistBlob.HistoryOpen(Model);
  38202. end else begin
  38203. Rec := Retrieve(HistJson.ModifiedRecord);
  38204. if Rec<>nil then
  38205. try // initialize BLOB with latest revision
  38206. HistBlob.fID := EngineAdd(TableHistoryIndex,JSON);
  38207. HistBlob.HistoryOpen(Model);
  38208. HistBlob.HistoryAdd(Rec,HistJson);
  38209. finally
  38210. FreeAndNil(Rec); // ignore partial SentDataJSON for this record
  38211. end;
  38212. end;
  38213. end;
  38214. end;
  38215. if (Rec=nil) or (HistBlob.fID=0) then
  38216. continue; // only append modifications to BLOB if valid
  38217. Rec.FillFrom(pointer(HistJson.SentDataJSON));
  38218. HistBlob.HistoryAdd(Rec,HistJson);
  38219. end;
  38220. if HistBlob.ModifiedRecord<>0 then
  38221. HistBlob.HistorySave(self,Rec);
  38222. SetLength(HistID,HistIDCount);
  38223. EngineDeleteWhere(TableHistoryIndex,WhereClause,TIDDynArray(HistID));
  38224. finally
  38225. HistJson.Free;
  38226. HistBlob.Free;
  38227. Rec.Free;
  38228. end;
  38229. finally
  38230. fAcquireExecution[execORMWrite].Safe.UnLock;
  38231. end;
  38232. end;
  38233. function TSQLRestServer.InternalUpdateEvent(aEvent: TSQLEvent; aTableIndex: integer;
  38234. aID: TID; const aSentData: RawUTF8; aIsBlobFields: PSQLFieldBits): boolean;
  38235. procedure DoTrackChanges;
  38236. var TableHistoryIndex: integer;
  38237. JSON: RawUTF8;
  38238. Event: TSQLHistoryEvent;
  38239. begin
  38240. case aEvent of
  38241. seAdd: Event := heAdd;
  38242. seUpdate: Event := heUpdate;
  38243. seDelete: Event := heDelete;
  38244. else exit;
  38245. end;
  38246. TableHistoryIndex := fTrackChangesHistoryTableIndex[aTableIndex];
  38247. fAcquireExecution[execORMWrite].Safe.Lock; // avoid race condition
  38248. try // low-level Add(TSQLRecordHistory) without cache
  38249. JSON := JSONEncode(['ModifiedRecord',aTableIndex+aID shl 6,'Event',ord(Event),
  38250. 'SentDataJSON',aSentData,'TimeStamp',ServerTimeStamp]);
  38251. EngineAdd(TableHistoryIndex,JSON);
  38252. { TODO: use a BATCH to speed up TSQLHistory storage }
  38253. if fTrackChangesHistory[TableHistoryIndex].CurrentRow>
  38254. fTrackChangesHistory[TableHistoryIndex].MaxSentDataJsonRow then begin
  38255. // gather & compress TSQLRecordHistory.SentDataJson into History BLOB
  38256. TrackChangesFlush(TSQLRecordHistoryClass(Model.Tables[TableHistoryIndex]));
  38257. fTrackChangesHistory[TableHistoryIndex].CurrentRow := 0;
  38258. end else
  38259. // fast append as JSON until reached MaxSentDataJsonRow
  38260. inc(fTrackChangesHistory[TableHistoryIndex].CurrentRow);
  38261. finally
  38262. fAcquireExecution[execORMWrite].Safe.UnLock;
  38263. end;
  38264. end;
  38265. begin
  38266. if aID<=0 then
  38267. result := false else
  38268. if aIsBlobFields<>nil then
  38269. // BLOB fields update
  38270. if (aEvent=seUpdateBlob) and Assigned(OnBlobUpdateEvent) then
  38271. result := OnBlobUpdateEvent(
  38272. self,seUpdate,fModel.Tables[aTableIndex],aID,aIsBlobFields^) else
  38273. result := true else begin
  38274. // simple fields modification
  38275. if (cardinal(aTableIndex)<fTrackChangesHistoryTableIndexCount) and
  38276. (fTrackChangesHistoryTableIndex[aTableIndex]>=0) then
  38277. DoTrackChanges;
  38278. if Assigned(OnUpdateEvent) then
  38279. result := OnUpdateEvent(self,aEvent,fModel.Tables[aTableIndex],aID,aSentData) else
  38280. result := true; // true on success, false if error (but action continues)
  38281. end;
  38282. end;
  38283. procedure TSQLRestServer.TrackChanges(const aTable: array of TSQLRecordClass;
  38284. aTableHistory: TSQLRecordHistoryClass; aMaxHistoryRowBeforeBlob,
  38285. aMaxHistoryRowPerRecord, aMaxUncompressedBlobSize: integer);
  38286. var t, tableIndex, TableHistoryIndex: integer;
  38287. begin
  38288. if (self=nil) or (high(aTable)<0) then
  38289. exit;
  38290. if aMaxHistoryRowBeforeBlob<=0 then // disable change tracking
  38291. TableHistoryIndex := -1 else begin
  38292. if aTableHistory=nil then
  38293. aTableHistory := TSQLRecordHistory;
  38294. TableHistoryIndex := Model.GetTableIndexExisting(aTableHistory);
  38295. end;
  38296. for t := 0 to high(aTable) do begin
  38297. tableIndex := Model.GetTableIndexExisting(aTable[t]);
  38298. if aTable[t].InheritsFrom(TSQLRecordHistory) then
  38299. raise EORMException.CreateUTF8('%.TrackChanges([%]) not allowed',[self,aTable[t]]);
  38300. if cardinal(tableIndex)<fTrackChangesHistoryTableIndexCount then begin
  38301. fTrackChangesHistoryTableIndex[tableIndex] := TableHistoryIndex;
  38302. if TableHistoryIndex>=0 then
  38303. with fTrackChangesHistory[TableHistoryIndex] do begin
  38304. if CurrentRow=0 then
  38305. CurrentRow := TableRowCount(aTableHistory);
  38306. MaxSentDataJsonRow := aMaxHistoryRowBeforeBlob;
  38307. MaxRevisionJSON := aMaxHistoryRowPerRecord;
  38308. MaxUncompressedBlobSize := aMaxUncompressedBlobSize;
  38309. end;
  38310. end;
  38311. end;
  38312. end;
  38313. function TSQLRestServer.InternalUpdateEventNeeded(aTableIndex: integer): boolean;
  38314. begin
  38315. result := (self<>nil) and (Assigned(OnUpdateEvent) or
  38316. ((cardinal(aTableIndex)<fTrackChangesHistoryTableIndexCount) and
  38317. (fTrackChangesHistoryTableIndex[aTableIndex]>=0)));
  38318. end;
  38319. function TSQLRestServer.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID;
  38320. var Rest: TSQLRest;
  38321. begin
  38322. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38323. if Rest=nil then
  38324. result := MainEngineAdd(TableModelIndex,SentData) else
  38325. result := Rest.EngineAdd(TableModelIndex,SentData);
  38326. end;
  38327. function TSQLRestServer.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
  38328. var Rest: TSQLRest;
  38329. begin
  38330. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38331. if Rest=nil then
  38332. result := MainEngineRetrieve(TableModelIndex,ID) else
  38333. result := Rest.EngineRetrieve(TableModelIndex,ID);
  38334. end;
  38335. function TSQLRestServer.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean;
  38336. ReturnedRowCount: PPtrInt): RawUTF8;
  38337. var Rest: TSQLRest;
  38338. StaticSQL: RawUTF8;
  38339. begin
  38340. StaticSQL := SQL;
  38341. Rest := InternalAdaptSQL(Model.GetTableIndexFromSQLSelect(SQL,false),StaticSQL);
  38342. if Rest=nil then
  38343. result := MainEngineList(SQL,ForceAJAX,ReturnedRowCount) else
  38344. result := Rest.EngineList(StaticSQL,ForceAJAX,ReturnedRowCount);
  38345. end;
  38346. function TSQLRestServer.EngineUpdate(TableModelIndex: integer; ID: TID;
  38347. const SentData: RawUTF8): boolean;
  38348. var Rest: TSQLRest;
  38349. begin
  38350. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38351. if Rest=nil then
  38352. result := MainEngineUpdate(TableModelIndex,ID,SentData) else
  38353. result := Rest.EngineUpdate(TableModelIndex,ID,SentData);
  38354. end;
  38355. function TSQLRestServer.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
  38356. var Rest: TSQLRest;
  38357. begin
  38358. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38359. if Rest=nil then
  38360. result := MainEngineDelete(TableModelIndex,ID) else
  38361. result := Rest.EngineDelete(TableModelIndex,ID);
  38362. if result then
  38363. if Model.TableProps[TableModelIndex].Props.RecordVersionField<>nil then
  38364. InternalRecordVersionDelete(TableModelIndex,ID,nil);
  38365. end;
  38366. function TSQLRestServer.EngineDeleteWhere(TableModelIndex: integer;
  38367. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  38368. var Rest: TSQLRest;
  38369. Batch: TSQLRestBatch;
  38370. i: integer;
  38371. begin
  38372. case length(IDs) of
  38373. 0: result := false;
  38374. 1: result := EngineDelete(TableModelIndex,IDs[0]);
  38375. else begin
  38376. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38377. if Rest=nil then
  38378. result := MainEngineDeleteWhere(TableModelIndex,SQLWhere,IDs) else
  38379. result := Rest.EngineDeleteWhere(TableModelIndex,SQLWhere,IDs);
  38380. if (Model.TableProps[TableModelIndex].Props.RecordVersionField=nil) or
  38381. not result then
  38382. exit;
  38383. Batch := TSQLRestBatch.Create(Self,Model.Tables[TableModelIndex],1000);
  38384. try
  38385. for i := 0 to high(IDs) do
  38386. InternalRecordVersionDelete(TableModelIndex,IDs[i],Batch);
  38387. BatchSend(Batch); // allow faster deletion for engines allowing it
  38388. finally
  38389. Batch.Free;
  38390. end;
  38391. end;
  38392. end;
  38393. end;
  38394. function TSQLRestServer.EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  38395. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  38396. var Rest: TSQLRest;
  38397. begin
  38398. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38399. if Rest=nil then
  38400. result := MainEngineRetrieveBlob(TableModelIndex,aID,BlobField,BlobData) else
  38401. result := Rest.EngineRetrieveBlob(TableModelIndex,aID,BlobField,BlobData);
  38402. end;
  38403. function TSQLRestServer.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  38404. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  38405. var Rest: TSQLRest;
  38406. begin
  38407. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38408. if Rest=nil then
  38409. result := MainEngineUpdateBlob(TableModelIndex,aID,BlobField,BlobData) else
  38410. result := Rest.EngineUpdateBlob(TableModelIndex,aID,BlobField,BlobData);
  38411. end;
  38412. function TSQLRestServer.EngineUpdateField(TableModelIndex: integer;
  38413. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  38414. var Rest: TSQLRest;
  38415. begin
  38416. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38417. if Rest=nil then
  38418. result := MainEngineUpdateField(TableModelIndex,SetFieldName,SetValue,
  38419. WhereFieldName,WhereValue) else
  38420. result := Rest.EngineUpdateField(TableModelIndex,SetFieldName,SetValue,
  38421. WhereFieldName,WhereValue);
  38422. end;
  38423. function TSQLRestServer.EngineUpdateFieldIncrement(TableModelIndex: integer;
  38424. ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
  38425. var Rest: TSQLRest;
  38426. begin
  38427. Rest := GetStaticDataServerOrVirtualTable(TableModelIndex);
  38428. if Rest=nil then
  38429. result := MainEngineUpdateFieldIncrement(TableModelIndex,ID,FieldName,Increment) else
  38430. result := Rest.EngineUpdateFieldIncrement(TableModelIndex,ID,FieldName,Increment);
  38431. end;
  38432. type
  38433. EORMBatchException = class(EORMException);
  38434. function TSQLRestServer.EngineBatchSend(Table: TSQLRecordClass;
  38435. const Data: RawUTF8; var Results: TIDDynArray; ExpectedResultsCount: integer): integer;
  38436. var EndOfObject: AnsiChar;
  38437. wasString, OK: boolean;
  38438. TableName, Value, ErrMsg: RawUTF8;
  38439. URIMethod, RunningBatchURIMethod: TSQLURIMethod;
  38440. RunningBatchRest, RunningRest: TSQLRest;
  38441. Sent, Method, MethodTable: PUTF8Char;
  38442. AutomaticTransactionPerRow: cardinal;
  38443. RowCountForCurrentTransaction: cardinal;
  38444. RunTableTransactions: array of TSQLRest;
  38445. RunMainTransaction: boolean;
  38446. ID: TID;
  38447. Count: integer;
  38448. timeoutTix: Int64;
  38449. batchOptions: TSQLRestBatchOptions;
  38450. RunTable, RunningBatchTable: TSQLRecordClass;
  38451. RunTableIndex,i,TableIndex: integer;
  38452. RunStatic: TSQLRest;
  38453. RunStaticKind: TSQLRestServerKind;
  38454. CurrentContext: TSQLRestServerURIContext;
  38455. counts: array[mPOST..mDELETE] of cardinal;
  38456. procedure PerformAutomaticCommit;
  38457. var i: integer;
  38458. begin
  38459. if RunningBatchRest<>nil then begin
  38460. RunningBatchRest.InternalBatchStop; // send pending rows before commit
  38461. RunningBatchRest := nil;
  38462. RunningBatchTable := nil;
  38463. end;
  38464. for i := 0 to high(RunTableTransactions) do
  38465. if RunTableTransactions[i]<>nil then begin
  38466. RunTableTransactions[i].Commit(CONST_AUTHENTICATION_NOT_USED,true);
  38467. RunTableTransactions[i] := nil;
  38468. if RunTableTransactions[i]=Self then
  38469. RunMainTransaction := false;
  38470. end;
  38471. RowCountForCurrentTransaction := 0;
  38472. end;
  38473. function IsNotAllowed: boolean;
  38474. begin
  38475. result := (CurrentContext<>nil) and
  38476. not CurrentContext.Call.RestAccessRights^.CanExecuteORMWrite(
  38477. URIMethod,RunTable,RunTableIndex,ID,CurrentContext);
  38478. end;
  38479. {$ifdef WITHLOG}
  38480. var Log: ISynLog; // for Enter auto-leave to work with FPC
  38481. begin
  38482. Log := fLogClass.Enter('EngineBatchSend % inlen=%',[Table,length(Data)],self);
  38483. {$else}
  38484. begin
  38485. {$endif}
  38486. Sent := pointer(Data);
  38487. if Sent=nil then
  38488. raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%,"")',[self,Table]);
  38489. CurrentContext := ServiceContext.Request;
  38490. if Table<>nil then begin
  38491. TableIndex := Model.GetTableIndexExisting(Table);
  38492. // unserialize expected sequence array as '{"Table":["cmd",values,...]}'
  38493. if not NextNotSpaceCharIs(Sent,'{') then
  38494. raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Missing {',[self]);
  38495. TableName := GetJSONPropName(Sent);
  38496. if (TableName='') or (Sent=nil) or
  38497. not IdemPropNameU(TableName,Model.TableProps[TableIndex].Props.SQLTableName) then
  38498. raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%): Wrong "Table":"%"',
  38499. [self,Table,TableName]);
  38500. end else // or '["cmd@Table":values,...]'
  38501. TableIndex := -1;
  38502. if not NextNotSpaceCharIs(Sent,'[') then
  38503. raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Missing [',[self]);
  38504. if IdemPChar(Sent,'"AUTOMATICTRANSACTIONPERROW",') then begin
  38505. inc(Sent,29);
  38506. AutomaticTransactionPerRow := GetNextItemCardinal(Sent,',');
  38507. end else
  38508. AutomaticTransactionPerRow := 0;
  38509. SetLength(RunTableTransactions,Model.TablesMax+1);
  38510. RunMainTransaction := false;
  38511. RowCountForCurrentTransaction := 0;
  38512. if IdemPChar(Sent,'"OPTIONS",') then begin
  38513. inc(Sent,10);
  38514. byte(batchOptions) := GetNextItemCardinal(Sent,',');
  38515. end else
  38516. byte(batchOptions) := 0;
  38517. MethodTable := nil;
  38518. RunningBatchRest := nil;
  38519. RunningBatchTable := nil;
  38520. RunningBatchURIMethod := mNone;
  38521. Count := 0;
  38522. FillcharFast(counts,SizeOf(counts),0);
  38523. fAcquireExecution[execORMWrite].fSafe.Lock; // multi thread protection
  38524. try // to protect automatic transactions and global write lock
  38525. try // to protect InternalBatchStart/Stop locking
  38526. repeat // main loop: process one POST/PUT/DELETE per iteration
  38527. // retrieve method name and associated (static) table
  38528. Method := GetJSONField(Sent,Sent,@wasString);
  38529. if (Sent=nil) or (Method=nil) or not wasString then
  38530. raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Missing CMD',[self]);
  38531. MethodTable := PosChar(Method,'@');
  38532. if MethodTable=nil then begin // e.g. '{"Table":[...,"POST",{object},...]}'
  38533. if TableIndex<0 then
  38534. raise EORMBatchException.CreateUTF8(
  38535. '%.EngineBatchSend: "..@Table" expected',[self]);
  38536. RunTableIndex := TableIndex;
  38537. RunTable := Table;
  38538. end else begin // e.g. '[...,"POST@Table",{object},...]'
  38539. RunTableIndex := Model.GetTableIndex(MethodTable+1);
  38540. if RunTableIndex<0 then
  38541. raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Unknown %',
  38542. [self,MethodTable]);
  38543. RunTable := Model.Tables[RunTableIndex];
  38544. end;
  38545. RunStatic := GetStaticDataServerOrVirtualTable(RunTableIndex,RunStaticKind);
  38546. if RunStatic=nil then
  38547. RunningRest := self else
  38548. RunningRest := RunStatic;
  38549. // get CRUD method and associated Value/ID
  38550. case IdemPCharArray(Method,['POST','PUT','DELETE','SIMPLE']) of
  38551. // IdemPCharArray() will ignore '@' char if appended after method name
  38552. 0: begin
  38553. // '{"Table":[...,"POST",{object},...]}' or '[...,"POST@Table",{object},...]'
  38554. URIMethod := mPOST;
  38555. Value := JSONGetObject(Sent,@ID,EndOfObject,true);
  38556. if Sent=nil then
  38557. raise EORMBatchException.CreateUTF8(
  38558. '%.EngineBatchSend: Wrong POST',[self]);
  38559. if IsNotAllowed then
  38560. raise EORMBatchException.CreateUTF8(
  38561. '%.EngineBatchSend: POST/Add not allowed on %',[self,RunTable]);
  38562. if not RecordCanBeUpdated(RunTable,ID,seAdd,@ErrMsg) then
  38563. raise EORMBatchException.CreateUTF8(
  38564. '%.EngineBatchSend: POST impossible: %',[self,ErrMsg]);
  38565. end;
  38566. 1: begin
  38567. // '{"Table":[...,"PUT",{object},...]}' or '[...,"PUT@Table",{object},...]'
  38568. URIMethod := mPUT;
  38569. Value := JSONGetObject(Sent,@ID,EndOfObject,false);
  38570. if (Sent=nil) or (Value='') or (ID<=0) then
  38571. raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Wrong PUT',[self]);
  38572. if IsNotAllowed then
  38573. raise EORMBatchException.CreateUTF8(
  38574. '%.EngineBatchSend: PUT/Update not allowed on %',[self,RunTable]);
  38575. end;
  38576. 2: begin
  38577. // '{"Table":[...,"DELETE",ID,...]}' or '[...,"DELETE@Table",ID,...]'
  38578. URIMethod := mDELETE;
  38579. ID := GetInt64(GetJSONField(Sent,Sent,@wasString,@EndOfObject));
  38580. if (ID<=0) or wasString then
  38581. raise EORMBatchException.CreateUTF8(
  38582. '%.EngineBatchSend: Wrong DELETE',[self]);
  38583. if IsNotAllowed then
  38584. raise EORMBatchException.CreateUTF8(
  38585. '%.EngineBatchSend: DELETE not allowed on %',[self,RunTable]);
  38586. if not RecordCanBeUpdated(RunTable,ID,seDelete,@ErrMsg) then
  38587. raise EORMBatchException.CreateUTF8(
  38588. '%.EngineBatchSend: DELETE impossible: "%"',[self,ErrMsg]);
  38589. end;
  38590. 3: begin
  38591. // '{"Table":[...,"SIMPLE",[values],...]}' or '[...,"SIMPLE@Table",[values],...]'
  38592. URIMethod := mPOST;
  38593. Value := Model.TableProps[RunTableIndex].Props.
  38594. SaveSimpleFieldsFromJsonArray(Sent,EndOfObject,true);
  38595. ID := 0; // no ID is never transmitted with simple fields
  38596. if (Sent=nil) or (Value='') then
  38597. raise EORMBatchException.CreateUTF8(
  38598. '%.EngineBatchSend: Wrong SIMPLE',[self]);
  38599. if IsNotAllowed then
  38600. raise EORMBatchException.CreateUTF8(
  38601. '%.EngineBatchSend: SIMPLE/Add not allowed on %',[self,RunTable]);
  38602. if not RecordCanBeUpdated(RunTable,0,seAdd,@ErrMsg) then
  38603. raise EORMBatchException.CreateUTF8(
  38604. '%.EngineBatchSend: SIMPLE/Add impossible: %',[self,ErrMsg]);
  38605. end;
  38606. else raise EORMBatchException.CreateUTF8(
  38607. '%.EngineBatchSend: Unknown "%" method',[self,Method]);
  38608. end;
  38609. if (Count=0) and (EndOfObject=']') then begin
  38610. // single operation do not need a transaction nor InternalBatchStart/Stop
  38611. AutomaticTransactionPerRow := 0;
  38612. SetLength(Results,1);
  38613. end else begin
  38614. // handle auto-committed transaction process
  38615. if AutomaticTransactionPerRow>0 then begin
  38616. if RowCountForCurrentTransaction=AutomaticTransactionPerRow then
  38617. PerformAutomaticCommit; // reached AutomaticTransactionPerRow chunk
  38618. inc(RowCountForCurrentTransaction);
  38619. if RunTableTransactions[RunTableIndex]=nil then
  38620. // initiate transaction for this table if not started yet
  38621. if (RunStatic<>nil) or not RunMainTransaction then begin
  38622. timeoutTix := GetTickCount64+2000;
  38623. repeat
  38624. if RunningRest.TransactionBegin(RunTable, // acquire transaction
  38625. CONST_AUTHENTICATION_NOT_USED) then begin
  38626. RunTableTransactions[RunTableIndex] := RunningRest;
  38627. if RunStatic=nil then
  38628. RunMainTransaction := true;
  38629. Break;
  38630. end;
  38631. if GetTickCount64>timeoutTix then
  38632. raise EORMBatchException.CreateUTF8(
  38633. '%.EngineBatchSend: %.TransactionBegin timeout',[self,RunningRest]);
  38634. SleepHiRes(1); // retry in 1 ms
  38635. until false;
  38636. end;
  38637. end;
  38638. // handle batch pending request sending (if table or method changed)
  38639. if (RunningBatchRest<>nil) and
  38640. ((RunTable<>RunningBatchTable) or (RunningBatchURIMethod<>URIMethod)) then begin
  38641. RunningBatchRest.InternalBatchStop; // send pending statements
  38642. RunningBatchRest := nil;
  38643. RunningBatchTable := nil;
  38644. end;
  38645. if (RunStatic<>nil) and (RunStatic<>RunningBatchRest) and
  38646. RunStatic.InternalBatchStart(URIMethod,batchOptions) then begin
  38647. RunningBatchRest := RunStatic;
  38648. RunningBatchTable := RunTable;
  38649. RunningBatchURIMethod := URIMethod;
  38650. end else
  38651. if (RunningBatchRest=nil) and (RunStatic=nil) and
  38652. InternalBatchStart(URIMethod,batchOptions) then begin
  38653. RunningBatchRest := self; // e.g. multi-insert in main SQlite3 engine
  38654. RunningBatchTable := RunTable;
  38655. RunningBatchURIMethod := URIMethod;
  38656. end;
  38657. if Count>=length(Results) then
  38658. SetLength(Results,Count+256+Count shr 3);
  38659. end;
  38660. // process CRUD method operation
  38661. Results[Count] := HTML_NOTMODIFIED;
  38662. case URIMethod of
  38663. mDELETE: begin
  38664. OK := EngineDelete(RunTableIndex,ID);
  38665. if OK then begin
  38666. if fCache<>nil then
  38667. fCache.NotifyDeletion(RunTableIndex,ID);
  38668. if (RunningBatchRest<>nil) or
  38669. AfterDeleteForceCoherency(RunTableIndex,ID) then
  38670. Results[Count] := HTML_SUCCESS; // 200 OK
  38671. end;
  38672. end;
  38673. mPOST: begin
  38674. ID := EngineAdd(RunTableIndex,Value);
  38675. Results[Count] := ID;
  38676. if (ID<>0) and (fCache<>nil) then
  38677. fCache.Notify(RunTableIndex,ID,Value,soInsert);
  38678. end;
  38679. mPUT: begin
  38680. OK := EngineUpdate(RunTableIndex,ID,Value);
  38681. if OK then begin
  38682. Results[Count] := HTML_SUCCESS; // 200 OK
  38683. if fCache<>nil then // JSON Value may be uncomplete -> delete from cache
  38684. fCache.NotifyDeletion(RunTableIndex,ID);
  38685. end;
  38686. end;
  38687. else raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Unknown "%" method',
  38688. [self,Method]);
  38689. end;
  38690. inc(Count);
  38691. inc(counts[URIMethod]);
  38692. until EndOfObject=']';
  38693. if (AutomaticTransactionPerRow>0) and (RowCountForCurrentTransaction>0) then
  38694. // send pending rows within transaction
  38695. PerformAutomaticCommit;
  38696. finally
  38697. if RunningBatchRest<>nil then
  38698. RunningBatchRest.InternalBatchStop; // send pending rows, and release Safe.Lock
  38699. fAcquireExecution[execORMWrite].fSafe.UnLock;
  38700. InternalLog('EngineBatchSend json=% add=% update=% delete=% %%',
  38701. [KB(length(Data)),counts[mPOST],counts[mPUT],counts[mDELETE],
  38702. MethodTable,Table],sllTrace);
  38703. end;
  38704. except
  38705. on Exception do begin
  38706. if (AutomaticTransactionPerRow>0) and (RowCountForCurrentTransaction>0) then begin
  38707. for i := 0 to high(RunTableTransactions) do
  38708. if RunTableTransactions[i]<>nil then
  38709. RunTableTransactions[i].RollBack(CONST_AUTHENTICATION_NOT_USED);
  38710. InternalLog('PARTIAL rollback of latest auto-committed transaction',sllWarning);
  38711. end;
  38712. raise;
  38713. end;
  38714. end;
  38715. if Table<>nil then begin // '{"Table":["cmd":values,...]}' format
  38716. if Sent=nil then
  38717. raise EORMBatchException.CreateUTF8('%.EngineBatchSend: % Truncated',[self,Table]);
  38718. while not (Sent^ in ['}',#0]) do inc(Sent);
  38719. if Sent^<>'}' then
  38720. raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%): Missing }',[self,Table]);
  38721. end;
  38722. // if we reached here, process was OK
  38723. SetLength(Results,Count);
  38724. result := HTML_SUCCESS;
  38725. end;
  38726. function CurrentServiceContext: TServiceRunningContext;
  38727. begin
  38728. result := ServiceContext;
  38729. end;
  38730. function CurrentServiceContextServer: TSQLRestServer;
  38731. begin
  38732. with PServiceRunningContext(@ServiceContext)^ do
  38733. if Request<>nil then
  38734. result := Request.Server else
  38735. result := nil;
  38736. end;
  38737. function ToText(gran: TSynMonitorUsageGranularity): PShortString;
  38738. begin
  38739. result := GetEnumName(TypeInfo(TSynMonitorUsageGranularity),ord(gran));
  38740. end;
  38741. function ToText(ft: TSQLFieldType): PShortString;
  38742. begin
  38743. result := GetEnumName(TypeInfo(TSQLFieldType),ord(ft));
  38744. end;
  38745. function ToText(tk: TTypeKind): PShortString;
  38746. begin
  38747. result := GetEnumName(TypeInfo(TTypeKind),ord(tk));
  38748. end;
  38749. function ToText(e: TSQLEvent): PShortString;
  38750. begin
  38751. result := GetEnumName(TypeInfo(TSQLEvent),ord(e));
  38752. end;
  38753. function ToText(he: TSQLHistoryEvent): PShortString;
  38754. begin
  38755. result := GetEnumName(TypeInfo(TSQLHistoryEvent),ord(he));
  38756. end;
  38757. function ToText(o: TSQLOccasion): PShortString;
  38758. begin
  38759. result := GetEnumName(TypeInfo(TSQLOccasion),ord(o));
  38760. end;
  38761. function ToText(dft: TSQLDBFieldType): PShortString;
  38762. begin
  38763. result := GetEnumName(TypeInfo(TSQLDBFieldType),ord(dft));
  38764. end;
  38765. function ToText(si: TServiceInstanceImplementation): PShortString;
  38766. begin
  38767. result := GetEnumName(TypeInfo(TServiceInstanceImplementation),ord(si));
  38768. end;
  38769. function ToText(cmd: TSQLRestServerURIContextCommand): PShortString;
  38770. begin
  38771. result := GetEnumName(TypeInfo(TSQLRestServerURIContextCommand),ord(cmd));
  38772. end;
  38773. function ToText(op: TSQLQueryOperator): PShortString;
  38774. begin
  38775. result := GetEnumName(TypeInfo(TSQLQueryOperator),ord(op));
  38776. end;
  38777. function ToText(V: TInterfaceMockSpyCheck): PShortString;
  38778. begin
  38779. result := GetEnumName(TypeInfo(TInterfaceMockSpyCheck),ord(V));
  38780. end;
  38781. function ToText(m: TSQLURIMethod): PShortString;
  38782. begin
  38783. result := GetEnumName(TypeInfo(TSQLURIMethod),ord(m));
  38784. end;
  38785. function ToText(o: TSynTableStatementOperator): PShortString;
  38786. begin
  38787. result := GetEnumName(TypeInfo(TSynTableStatementOperator),ord(o));
  38788. end;
  38789. function ToText(t: TSQLVirtualTableTransaction): PShortString;
  38790. begin
  38791. result := GetEnumName(TypeInfo(TSQLVirtualTableTransaction),ord(t));
  38792. end;
  38793. { TSQLRestClientURIDll }
  38794. constructor TSQLRestClientURIDll.Create(aModel: TSQLModel; const DllName: TFileName);
  38795. var aRequest: TURIMapRequest;
  38796. aDLL: cardinal;
  38797. begin
  38798. {$ifdef KYLIX3}
  38799. aDLL := LoadLibrary(pointer(DllName));
  38800. {$else}
  38801. {$ifndef MSWINDOWS}
  38802. aDLL := LoadLibrary(DllName);
  38803. {$else}
  38804. aDLL := LoadLibrary(pointer(DllName));
  38805. {$endif}
  38806. {$endif}
  38807. if aDLL=0 then
  38808. raise ECommunicationException.CreateUTF8('%.Create: LoadLibrary(%)',[self,DllName]);
  38809. aRequest := GetProcAddress(aDLL,'URIRequest');
  38810. if (@aRequest=nil) or (aRequest(nil,nil,nil,nil,nil).Lo<>HTML_NOTFOUND) then begin
  38811. FreeLibrary(aDLL);
  38812. raise ECommunicationException.CreateUTF8(
  38813. '%.Create: % doesn''t export a valid URIRequest() function',[self,DllName]);
  38814. end;
  38815. Create(aModel,aRequest);
  38816. fLibraryHandle := aDLL;
  38817. end;
  38818. constructor TSQLRestClientURIDll.Create(aModel: TSQLModel; aRequest: TURIMapRequest);
  38819. begin
  38820. inherited Create(aModel);
  38821. Func := aRequest;
  38822. end;
  38823. destructor TSQLRestClientURIDll.Destroy;
  38824. begin
  38825. if fLibraryHandle<>0 then
  38826. FreeLibrary(fLibraryHandle);
  38827. inherited;
  38828. end;
  38829. procedure TSQLRestClientURIDll.InternalURI(var Call: TSQLRestURIParams);
  38830. var result: Int64Rec;
  38831. pHead, pResp: PUTF8Char;
  38832. begin
  38833. if @Func=nil then begin
  38834. Call.OutStatus := HTML_NOTIMPLEMENTED; // 501 (no valid application or library)
  38835. exit;
  38836. end;
  38837. pResp := nil;
  38838. pHead := nil;
  38839. try
  38840. result := Func(pointer(Call.Url),pointer(Call.Method),pointer(Call.InBody),
  38841. @pResp,@pHead);
  38842. Call.OutStatus := result.Lo;
  38843. Call.OutInternalState := result.Hi;
  38844. if pHead<>nil then
  38845. Call.OutHead := pHead;
  38846. if pResp<>nil then
  38847. Call.OutBody := pResp;
  38848. finally // always release response memory allocated by the server
  38849. if pResp<>nil then
  38850. {$ifdef MSWINDOWS}
  38851. if not USEFASTMM4ALLOC then
  38852. GlobalFree(PtrUInt(pResp)) else
  38853. {$endif}
  38854. Freemem(pResp);
  38855. if pHead<>nil then
  38856. {$ifdef MSWINDOWS}
  38857. if not USEFASTMM4ALLOC then
  38858. GlobalFree(PtrUInt(pHead)) else
  38859. {$endif}
  38860. Freemem(pHead);
  38861. end;
  38862. end;
  38863. function TSQLRestClientURIDll.InternalCheckOpen: boolean;
  38864. begin
  38865. result := true; // success
  38866. end;
  38867. procedure TSQLRestClientURIDll.InternalClose;
  38868. begin
  38869. end;
  38870. { TSQLRestClientRedirect }
  38871. constructor TSQLRestClientRedirect.Create(aModel: TSQLModel);
  38872. begin
  38873. inherited Create(aModel);
  38874. fModel.Owner := self;
  38875. end;
  38876. constructor TSQLRestClientRedirect.Create(aRedirected: TSQLRest);
  38877. begin
  38878. if aRedirected=nil then
  38879. raise EORMException.CreateUTF8('%.Create(nil)',[self]);
  38880. Create(TSQLModel.Create(aRedirected.Model));
  38881. RedirectTo(aRedirected);
  38882. end;
  38883. constructor TSQLRestClientRedirect.CreateOwned(aRedirected: TSQLRestServer);
  38884. begin
  38885. Create(aRedirected);
  38886. fPrivateGarbageCollector.Add(aRedirected);
  38887. end;
  38888. procedure TSQLRestClientRedirect.RedirectTo(aRedirected: TSQLRest);
  38889. begin
  38890. fSafe.Enter;
  38891. try
  38892. fRedirectedClient := nil;
  38893. fRedirectedServer := nil;
  38894. if aRedirected=nil then
  38895. exit; // redirection disabled
  38896. if aRedirected.InheritsFrom(TSQLRestServer) then
  38897. fRedirectedServer := aRedirected as TSQLRestServer else
  38898. if aRedirected.InheritsFrom(TSQLRestClientURI) then
  38899. fRedirectedClient := aRedirected as TSQLRestClientURI else
  38900. raise EORMException.CreateUTF8('%.RedirectTo: % should be either % or %',
  38901. [self,aRedirected,TSQLRestServer,TSQLRestClientURI]);
  38902. finally
  38903. fSafe.Leave;
  38904. end;
  38905. end;
  38906. function TSQLRestClientRedirect.InternalCheckOpen: boolean;
  38907. begin
  38908. result := Assigned(fRedirectedServer) or Assigned(fRedirectedClient);
  38909. end;
  38910. procedure TSQLRestClientRedirect.InternalClose;
  38911. begin
  38912. end;
  38913. procedure TSQLRestClientRedirect.InternalURI(var Call: TSQLRestURIParams);
  38914. begin
  38915. fSafe.Enter;
  38916. try
  38917. if Assigned(fRedirectedServer) then
  38918. fRedirectedServer.URI(Call) else
  38919. if Assigned(fRedirectedClient) then
  38920. // hook to access InternalURI() protected method
  38921. TSQLRestClientRedirect(fRedirectedClient).InternalURI(Call) else
  38922. Call.OutStatus := HTML_GATEWAYTIMEOUT;
  38923. finally
  38924. fSafe.Leave;
  38925. end;
  38926. end;
  38927. {$ifdef MSWINDOWS}
  38928. {$ifdef ANONYMOUSNAMEDPIPE}
  38929. // it should be necessary to Edit settings under Local Security Policy -> Local
  38930. // policies -> Security options -> Edit settings under "Network access" to allow
  38931. // for anonymous connections.
  38932. // BUT even with the pipe name added to the
  38933. // SYSTEM\CurrentControlSet\Services\lanmanserver\parameters\NullSessionPipes
  38934. // registry key, code below didn't work
  38935. function GetUserSid(var SID: PSID; var Token: THandle): boolean;
  38936. var TokenUserSize: DWORD;
  38937. TokenUserP: PSIDAndAttributes;
  38938. begin
  38939. result := false;
  38940. if not OpenThreadToken(GetCurrentThread,TOKEN_QUERY,True,Token) then
  38941. if (GetLastError <> ERROR_NO_TOKEN) or
  38942. not OpenProcessToken(GetCurrentProcess,TOKEN_QUERY,Token) then
  38943. exit;
  38944. TokenUserP := nil;
  38945. TokenUserSize := 0;
  38946. try
  38947. if not GetTokenInformation(Token,TokenUser,nil,0,TokenUserSize) and
  38948. (GetLastError <> ERROR_INSUFFICIENT_BUFFER) then
  38949. exit;
  38950. TokenUserP := AllocMem(TokenUserSize);
  38951. if not GetTokenInformation(Token,TokenUser,TokenUserP,TokenUserSize,TokenUserSize) then
  38952. exit;
  38953. SID := TokenUserP^.Sid;
  38954. result := true;
  38955. finally
  38956. FreeMem(TokenUserP);
  38957. end;
  38958. end;
  38959. {$ALIGN ON}
  38960. type
  38961. ACE_HEADER = record
  38962. AceType: BYTE;
  38963. AceFlags: BYTE;
  38964. AceSize: WORD;
  38965. end;
  38966. ACCESS_ALLOWED_ACE = record
  38967. Header: ACE_HEADER;
  38968. Mask: ACCESS_MASK;
  38969. SidStart: DWORD;
  38970. end;
  38971. {$A8}
  38972. procedure InitializeSecurity(var SA: TSecurityAttributes; var SD);
  38973. const
  38974. SECURITY_NT_AUTHORITY: TSIDIdentifierAuthority = (Value: (0, 0, 0, 0, 0, 5));
  38975. SECURITY_ANONYMOUS_LOGON_RID = ($00000007);
  38976. ACL_REVISION = 2;
  38977. var pSidAnonymous, pSidOwner: PSID;
  38978. dwAclSize: integer;
  38979. ACLP: PACL;
  38980. Token: THandle;
  38981. begin
  38982. FillcharFast(SD,SECURITY_DESCRIPTOR_MIN_LENGTH,0);
  38983. // Initialize the new security descriptor
  38984. if InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION) and
  38985. GetUserSid(pSidOwner,Token) then begin
  38986. AllocateAndInitializeSid(SECURITY_NT_AUTHORITY,1,
  38987. SECURITY_ANONYMOUS_LOGON_RID,0,0,0,0,0,0,0,pSidAnonymous);
  38988. try
  38989. dwAclSize := sizeof(TACL) +
  38990. 2 * ( sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) ) +
  38991. GetLengthSid(pSidAnonymous) + GetLengthSid(pSidOwner) ;
  38992. ACLP := AllocMem(dwAclSize);
  38993. try
  38994. InitializeAcl(ACLP^,dwAclSize,ACL_REVISION);
  38995. if not AddAccessAllowedAce(ACLP^,ACL_REVISION,
  38996. GENERIC_ALL,pSidOwner) then
  38997. exit;
  38998. if not AddAccessAllowedAce(ACLP^,ACL_REVISION,
  38999. GENERIC_READ or GENERIC_WRITE,pSidAnonymous) then
  39000. exit;
  39001. if SetSecurityDescriptorDacl(@SD,true,ACLP,false) then begin
  39002. // Set up the security attributes structure
  39003. SA.nLength := sizeof(TSecurityAttributes);
  39004. SA.lpSecurityDescriptor := @SD;
  39005. SA.bInheritHandle := true;
  39006. exit; // mark OK
  39007. end;
  39008. finally
  39009. FreeMem(ACLP);
  39010. end;
  39011. finally
  39012. FreeSid(pSidAnonymous);
  39013. CloseHandle(Token);
  39014. end;
  39015. end;
  39016. FillcharFast(SA,sizeof(SA),0); // mark error: no security
  39017. end;
  39018. {$else}
  39019. {$ifndef NOSECURITYFORNAMEDPIPECLIENTS}
  39020. {$if CompilerVersion >= 22.0} // fix Delphi XE incompatilibility
  39021. function InitializeSecurityDescriptor(pSecurityDescriptor: PSecurityDescriptor;
  39022. dwRevision: DWORD): BOOL; stdcall; external advapi32;
  39023. function SetSecurityDescriptorDacl(pSecurityDescriptor: PSecurityDescriptor;
  39024. bDaclPresent: BOOL; pDacl: PACL; bDaclDefaulted: BOOL): BOOL; stdcall; external advapi32;
  39025. {$ifend}
  39026. procedure InitializeSecurity(var SA: TSecurityAttributes; var SD);
  39027. begin
  39028. FillcharFast(SD,SECURITY_DESCRIPTOR_MIN_LENGTH,0);
  39029. // Initialize the new security descriptor
  39030. if InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION) then begin
  39031. // Add a NULL descriptor ACL to the security descriptor
  39032. if SetSecurityDescriptorDacl(@SD, true, nil, false) then begin
  39033. // Set up the security attributes structure
  39034. SA.nLength := sizeof(TSecurityAttributes);
  39035. SA.lpSecurityDescriptor := @SD;
  39036. SA.bInheritHandle := true;
  39037. exit; // mark OK
  39038. end;
  39039. end;
  39040. FillcharFast(SA,sizeof(SA),0); // mark error: no security
  39041. end;
  39042. {$endif NOSECURITYFORNAMEDPIPECLIENTS}
  39043. {$endif ANONYMOUSNAMEDPIPE}
  39044. { TSQLRestServerNamedPipe }
  39045. constructor TSQLRestServerNamedPipe.Create(aServer: TSQLRestServer;
  39046. const PipeName: TFileName);
  39047. begin
  39048. fServer := aServer;
  39049. fPipeName := PipeName;
  39050. fChild := TList.Create;
  39051. inherited Create(aServer,false,false);
  39052. end;
  39053. destructor TSQLRestServerNamedPipe.Destroy;
  39054. var i: integer;
  39055. begin
  39056. for i := 0 to fChild.Count-1 do // close any still opened pipe
  39057. if fChild[i]<>nil then begin
  39058. {writeln('fChildCount=',fChildCount,' TSQLRestServerNamedPipeResponse=',
  39059. integer(TSQLRestServerNamedPipeResponse),'.Terminated=',
  39060. BoolToStr(TSQLRestServerNamedPipeResponse(fChild[i]).Terminated,true));}
  39061. TSQLRestServerNamedPipeResponse(fChild[i]).Terminate;
  39062. end;
  39063. while fChildCount>0 do
  39064. SleepHiRes(64); // wait for all TSQLRestServerNamedPipeResponse.Destroy
  39065. fChild.Free;
  39066. inherited;
  39067. end;
  39068. procedure TSQLRestServerNamedPipe.InternalExecute;
  39069. {$ifdef FPC}
  39070. const PIPE_UNLIMITED_INSTANCES = 255;
  39071. {$endif}
  39072. var aPipe: cardinal;
  39073. Available: cardinal;
  39074. {$ifndef NOSECURITYFORNAMEDPIPECLIENTS}
  39075. fPipeSecurityAttributes: TSecurityAttributes;
  39076. fPipeSecurityDescriptor: array[0..SECURITY_DESCRIPTOR_MIN_LENGTH] of byte;
  39077. {$endif}
  39078. begin // see http://msdn.microsoft.com/en-us/library/aa365588(v=VS.85).aspx
  39079. //writeln('TSQLRestServerNamedPipe=',integer(TSQLRestServerNamedPipe),'.Execute');
  39080. {$ifndef NOSECURITYFORNAMEDPIPECLIENTS}
  39081. InitializeSecurity(fPipeSecurityAttributes,fPipeSecurityDescriptor);
  39082. {$endif}
  39083. while not Terminated do begin
  39084. //writeln('TSQLRestServerNamedPipe.CreateNamedPipe(',fPipeName,')');
  39085. aPipe := CreateNamedPipe(pointer(fPipeName),
  39086. PIPE_ACCESS_DUPLEX,
  39087. PIPE_TYPE_BYTE or PIPE_READMODE_BYTE or PIPE_WAIT,
  39088. PIPE_UNLIMITED_INSTANCES, 0, 0, 0,
  39089. {$ifdef NOSECURITYFORNAMEDPIPECLIENTS}nil{$else}@fPipeSecurityAttributes{$endif});
  39090. if aPipe=cardinal(INVALID_HANDLE_VALUE) then
  39091. break;
  39092. while not Terminated do
  39093. if PeekNamedPipe(aPipe,nil,0,nil,@Available,nil) then
  39094. if (Available>=4) then begin
  39095. // PeekNamedPipe() made an implicit ConnectNamedPipe(aPipe,nil)
  39096. InterlockedIncrement(fChildCount);
  39097. TSQLRestServerNamedPipeResponse.Create(fServer,self,aPipe);
  39098. aPipe := 0; // aPipe will be closed in TSQLRestServerNamedPipeResponse
  39099. break;
  39100. end
  39101. else break // invalid request
  39102. else SleepHiRes(128); // doesn't slow down connection but decreases CSwitch
  39103. if aPipe<>0 then begin
  39104. DisconnectNamedPipe(aPipe);
  39105. CloseHandle(aPipe);
  39106. end;
  39107. end;
  39108. end;
  39109. { TSQLRestServerNamedPipeResponse }
  39110. constructor TSQLRestServerNamedPipeResponse.Create(aServer: TSQLRestServer;
  39111. aMasterThread: TSQLRestServerNamedPipe; aPipe: cardinal);
  39112. begin
  39113. fServer := aServer;
  39114. fMasterThread := aMasterThread;
  39115. with fMasterThread.fChild do begin
  39116. fMasterThreadChildIndex := IndexOf(nil); // get free position in fChild[]
  39117. if fMasterThreadChildIndex<0 then
  39118. fMasterThreadChildIndex := Add(self) else
  39119. Items[fMasterThreadChildIndex] := self;
  39120. end;
  39121. fPipe := aPipe;
  39122. {$ifdef LVCL}
  39123. FOnTerminate := fServer.EndCurrentThread;
  39124. {$endif}
  39125. FreeOnTerminate := true;
  39126. inherited Create(fServer,false,false);
  39127. end;
  39128. destructor TSQLRestServerNamedPipeResponse.Destroy;
  39129. begin
  39130. if fMasterThread<>nil then
  39131. with fMasterThread do begin
  39132. fChild[fMasterThreadChildIndex] := nil;
  39133. InterlockedDecrement(fChildCount);
  39134. end;
  39135. inherited;
  39136. end;
  39137. procedure TSQLRestServerNamedPipeResponse.InternalExecute;
  39138. var call: TSQLRestURIParams;
  39139. Code: integer;
  39140. Ticks64, Sleeper64, ClientTimeOut64: Int64;
  39141. Header: RawUTF8;
  39142. Available: cardinal;
  39143. begin
  39144. if (fPipe=0) or (fPipe=Cardinal(INVALID_HANDLE_VALUE)) or (fServer=nil) then
  39145. exit;
  39146. Header := 'RemoteIP: 127.0.0.1';
  39147. call.Init;
  39148. call.LowLevelConnectionID := fPipe;
  39149. Ticks64 := 0;
  39150. Sleeper64 := 0;
  39151. ClientTimeOut64 := GetTickCount64+30*60*1000; // disconnect after 30 min of inactivity
  39152. try
  39153. while not Terminated do
  39154. if // (WaitForSingleObject(fPipe,200)=WAIT_OBJECT_0) = don't wait
  39155. PeekNamedPipe(fPipe,nil,0,nil,@Available,nil) and (Available>=4) then begin
  39156. FileRead(fPipe,Code,4);
  39157. if (Code=integer(MAGIC_SYN)) // magic word for URI like request
  39158. and not Terminated then
  39159. try
  39160. call.Url := ReadString(fPipe);
  39161. call.Method := ReadString(fPipe);
  39162. call.InHead := ReadString(fPipe);
  39163. if call.InHead='' then
  39164. call.InHead := Header else
  39165. call.InHead := call.InHead+#13#10+Header;
  39166. call.InBody := ReadString(fPipe);
  39167. call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS;
  39168. call.OutHead := ''; // may not be reset explicitly by fServer.URI()
  39169. call.OutBody := '';
  39170. // it's up to URI overridden method to implement access rights
  39171. fServer.URI(call);
  39172. FileWrite(fPipe,call.OutStatus,sizeof(cardinal));
  39173. FileWrite(fPipe,call.OutInternalState,sizeof(cardinal));
  39174. WriteString(fPipe,call.OutHead);
  39175. WriteString(fPipe,call.OutBody);
  39176. FlushFileBuffers(fPipe); // Flush the pipe to allow the client to read
  39177. Ticks64 := GetTickCount64+20; // start sleeping after 20 ms
  39178. ClientTimeOut64 := Ticks64+30*60*1000;
  39179. Sleeper64 := 0;
  39180. SleepHiRes(0);
  39181. except
  39182. on Exception do // error in ReadString() or fServer.URI()
  39183. break; // disconnect client
  39184. end else
  39185. break; // invalid magic word: disconnect client
  39186. end else
  39187. if (Ticks64=0) or (GetTickCount64>Ticks64) then begin
  39188. if Sleeper64<128 then
  39189. inc(Sleeper64,16);
  39190. SleepHiRes(Sleeper64); // doesn't slow down connection but decreases CSwitch
  39191. Ticks64 := 0;
  39192. if GetTickCount64>ClientTimeOut64 then
  39193. break; // disconnect client after 30 min of inactivity
  39194. end else
  39195. SleepHiRes(0);
  39196. finally
  39197. DisconnectNamedPipe(fPipe);
  39198. CloseHandle(fPipe);
  39199. end;
  39200. end;
  39201. { TSQLRestClientURINamedPipe }
  39202. function ImpersonateAnonymousToken(ThreadHandle: THANDLE): BOOL; stdcall; external advapi32;
  39203. constructor TSQLRestClientURINamedPipe.Create(aModel: TSQLModel;
  39204. const ApplicationName: TFileName);
  39205. begin
  39206. inherited Create(aModel);
  39207. if {$ifdef UNICODE}IdemPCharW{$else}IdemPChar{$endif}(pointer(ApplicationName),'\\') then
  39208. fPipeName := ApplicationName else // caller specified a full path
  39209. fPipeName := ServerPipeNamePrefix+ApplicationName;
  39210. end;
  39211. procedure TSQLRestClientURINamedPipe.DefinitionTo(Definition: TSynConnectionDefinition);
  39212. begin
  39213. if Definition=nil then
  39214. exit;
  39215. inherited DefinitionTo(Definition); // write Kind + User/Password
  39216. Definition.ServerName := StringToUTF8(fPipeName);
  39217. end;
  39218. constructor TSQLRestClientURINamedPipe.RegisteredClassCreateFrom(aModel: TSQLModel;
  39219. aDefinition: TSynConnectionDefinition);
  39220. begin
  39221. Create(aModel,UTF8ToString(aDefinition.ServerName));
  39222. inherited RegisteredClassCreateFrom(aModel,aDefinition); // call SetUser()
  39223. end;
  39224. function TSQLRestClientURINamedPipe.InternalCheckOpen: boolean;
  39225. procedure InternalCreateClientPipe;
  39226. var Pipe: THandle;
  39227. StartTime64: Int64;
  39228. {$ifdef WITHLOG}
  39229. Log: ISynLog;
  39230. {$endif}
  39231. procedure CreatePipe;
  39232. begin
  39233. Pipe := CreateFile(pointer(fPipeName), GENERIC_READ or GENERIC_WRITE,
  39234. {$ifdef ANONYMOUSNAMEDPIPE}
  39235. FILE_SHARE_READ or FILE_SHARE_WRITE,
  39236. nil, OPEN_EXISTING, SECURITY_SQOS_PRESENT or SECURITY_ANONYMOUS, 0);
  39237. {$else}
  39238. 0, {$ifdef NOSECURITYFORNAMEDPIPECLIENTS}nil{$else}@fPipeSecurityAttributes{$endif},
  39239. OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_OVERLAPPED, 0);
  39240. {$endif}
  39241. end;
  39242. begin
  39243. {$ifdef WITHLOG}
  39244. Log := fLogClass.Enter(self);
  39245. {$endif}
  39246. {$ifdef ANONYMOUSNAMEDPIPE}
  39247. if not ImpersonateAnonymousToken(GetCurrentThread) then
  39248. raise Exception.Create('ImpersonateAnonymousToken');
  39249. try
  39250. {$else}
  39251. {$ifndef NOSECURITYFORNAMEDPIPECLIENTS}
  39252. InitializeSecurity(fPipeSecurityAttributes,fPipeSecurityDescriptor);
  39253. {$endif}
  39254. {$endif}
  39255. StartTime64 := GetTickCount64;
  39256. CreatePipe;
  39257. while (Pipe=INVALID_HANDLE_VALUE) and (GetLastError=ERROR_FILE_NOT_FOUND) do begin
  39258. SleepHiRes(10); // wait for TSQLRestServerNamedPipe.EngineExecute to be reached
  39259. CreatePipe;
  39260. if (Pipe<>INVALID_HANDLE_VALUE) or (GetTickCount64>StartTime64+500) then
  39261. break;
  39262. end;
  39263. StartTime64 := GetTickCount64;
  39264. if (Pipe=INVALID_HANDLE_VALUE) and (GetLastError=ERROR_PIPE_BUSY) then
  39265. InternalLog('Busy % -> retry',[fPipeName],sllDebug);
  39266. repeat
  39267. SleepHiRes(10);
  39268. if WaitNamedPipe(pointer(fPipeName),50) then begin
  39269. CreatePipe;
  39270. if GetLastError<>ERROR_PIPE_BUSY then
  39271. break;
  39272. end;
  39273. until GetTickCount64>StartTime64+2000;
  39274. if Pipe=INVALID_HANDLE_VALUE then begin
  39275. InternalLog('when connecting to % after % ms',
  39276. [fPipeName,GetTickCount64-StartTime64],sllLastError);
  39277. exit;
  39278. end;
  39279. {$ifdef ANONYMOUSNAMEDPIPE}
  39280. finally
  39281. RevertToSelf; // we just needed to be anonymous during pipe connection
  39282. end;
  39283. {$endif}
  39284. InternalLog('Connected to %',[fPipeName],sllDebug);
  39285. fServerPipe := Pipe;
  39286. end;
  39287. begin
  39288. if fServerPipe<>0 then begin
  39289. result := true;
  39290. exit; // only reconnect if forced by InternalClose call or at first access
  39291. end;
  39292. InternalCreateClientPipe; // local sub-procedure to reduce stack overhead
  39293. result := fServerPipe<>0;
  39294. end;
  39295. procedure TSQLRestClientURINamedPipe.InternalClose;
  39296. begin
  39297. if fServerPipe<>0 then begin // inherited; may use pipe -> close after
  39298. WriteString(fServerPipe,''); // send integer=0 -> force server disconnect
  39299. FileClose(fServerPipe);
  39300. end;
  39301. end;
  39302. procedure TSQLRestClientURINamedPipe.InternalURI(var Call: TSQLRestURIParams);
  39303. var Card: cardinal;
  39304. {.$define TSQLRestClientURIDll_TIMEOUT} // to be tried over slow networks if errors
  39305. {$ifdef TSQLRestClientURIDll_TIMEOUT}
  39306. i: integer;
  39307. {$endif}
  39308. {$ifdef WITHLOG}
  39309. Log: ISynLog;
  39310. {$endif}
  39311. begin
  39312. {$ifdef WITHLOG}
  39313. Log := fLogClass.Enter(self);
  39314. {$endif}
  39315. Call.OutStatus := HTML_NOTIMPLEMENTED; // 501 (no valid application or library)
  39316. fSafe.Enter;
  39317. try
  39318. if InternalCheckOpen then
  39319. try
  39320. Card := MAGIC_SYN; // magic word
  39321. if FileWrite(fServerPipe,Card,4)<>4 then begin
  39322. SleepHiRes(0);
  39323. WaitNamedPipe(pointer(fPipeName),200);
  39324. if FileWrite(fServerPipe,Card,4)<>4 then begin // pipe may be broken
  39325. SleepHiRes(10);
  39326. FileClose(fServerPipe);
  39327. fServerPipe := 0;
  39328. if not InternalCheckOpen then // recreate connection
  39329. exit;
  39330. if (fServerPipe=Invalid_Handle_Value) or
  39331. (FileWrite(fServerPipe,Card,4)<>4) then begin
  39332. Card := GetLastError;
  39333. InternalLog('reconnecting to %',[fPipeName],sllLastError);
  39334. if fServerPipe<>Invalid_Handle_Value then
  39335. FileClose(fServerPipe);
  39336. fServerPipe := 0;
  39337. exit; // no existing pipe
  39338. end;
  39339. end;
  39340. end;
  39341. // send the request
  39342. WriteString(fServerPipe,Call.Url);
  39343. WriteString(fServerPipe,Call.Method);
  39344. WriteString(fServerPipe,Call.InHead);
  39345. WriteString(fServerPipe,Call.InBody);
  39346. FlushFileBuffers(fServerPipe);
  39347. // receive the answer
  39348. {$ifdef TSQLRestClientURIDll_TIMEOUT}
  39349. for i := 0 to 25 do // wait up to 325 ms
  39350. if PeekNamedPipe(fServerPipe,nil,0,nil,@Card,nil) and
  39351. (Card>=sizeof(Int64)) then begin
  39352. FileRead(fServerPipe,Call.OutStatus,sizeof(cardinal));
  39353. FileRead(fServerPipe,Call.OutInternalState,sizeof(cardinal));
  39354. Call.OutHead := ReadString(fServerPipe);
  39355. Call.OutBody := ReadString(fServerPipe);
  39356. exit;
  39357. end else
  39358. SleepHiRes(i);
  39359. Call.OutStatus := HTML_TIMEOUT; // 408 Request Timeout Error
  39360. {$else}
  39361. if FileRead(fServerPipe,Call.OutStatus,sizeof(cardinal))=sizeof(cardinal) then begin
  39362. // FileRead() waits till response arrived (or pipe is broken)
  39363. FileRead(fServerPipe,Call.OutInternalState,sizeof(cardinal));
  39364. Call.OutHead := ReadString(fServerPipe);
  39365. Call.OutBody := ReadString(fServerPipe);
  39366. end else
  39367. Call.OutStatus := HTML_NOTFOUND;
  39368. {$endif}
  39369. except
  39370. on E: Exception do begin // error in ReadString()
  39371. InternalLog('% for PipeName=%',[E,fPipeName],sllLastError);
  39372. Call.OutStatus := HTML_NOTIMPLEMENTED; // 501 (no valid application or library)
  39373. WriteString(fServerPipe,''); // try to notify the server of client logout
  39374. FileClose(fServerPipe);
  39375. fServerPipe := 0;
  39376. end;
  39377. end;
  39378. finally
  39379. fSafe.Leave;
  39380. end;
  39381. with Call do
  39382. InternalLog('% % status=% state=%',[method,url,OutStatus,OutInternalState],sllClient);
  39383. end;
  39384. {$endif MSWINDOWS}
  39385. { TSQLRestServerMonitor }
  39386. constructor TSQLRestServerMonitor.Create(aServer: TSQLRestServer);
  39387. begin
  39388. if aServer=nil then
  39389. raise EORMException.CreateUTF8('%.Create(nil)',[self]);
  39390. inherited Create(aServer.Model.Root);
  39391. fServer := aServer;
  39392. SetLength(fPerTable[false],length(aServer.Model.Tables));
  39393. SetLength(fPerTable[true],length(aServer.Model.Tables));
  39394. fStartDate := NowUTCToString;
  39395. end;
  39396. destructor TSQLRestServerMonitor.Destroy;
  39397. begin
  39398. ObjArrayClear(fPerTable[false]);
  39399. ObjArrayClear(fPerTable[true]);
  39400. inherited;
  39401. end;
  39402. procedure TSQLRestServerMonitor.ProcessSuccess(IsOutcomingFile: boolean);
  39403. begin
  39404. EnterCriticalSection(fLock);
  39405. try
  39406. inc(fSuccess);
  39407. if IsOutcomingFile then
  39408. inc(fOutcomingFiles);
  39409. Changed;
  39410. finally
  39411. LeaveCriticalSection(fLock);
  39412. end;
  39413. end;
  39414. procedure TSQLRestServerMonitor.NotifyORM(aMethod: TSQLURIMethod);
  39415. begin
  39416. EnterCriticalSection(fLock);
  39417. try
  39418. case aMethod of
  39419. mGET,mLOCK: inc(fRead);
  39420. mPOST: inc(fCreated);
  39421. mPUT: inc(fUpdated);
  39422. mDELETE: inc(fDeleted);
  39423. end;
  39424. Changed;
  39425. finally
  39426. LeaveCriticalSection(fLock);
  39427. end;
  39428. end;
  39429. procedure TSQLRestServerMonitor.NotifyORMTable(TableIndex, DataSize: integer;
  39430. Write: boolean; const MicroSecondsElapsed: QWord);
  39431. const RW: array[boolean] of RawUTF8 = ('.read','.write');
  39432. var st: TSynMonitorWithSize;
  39433. begin
  39434. if TableIndex<0 then
  39435. exit;
  39436. EnterCriticalSection(fLock);
  39437. try
  39438. if TableIndex>=length(fPerTable[Write]) then
  39439. // tables may have been added after Create()
  39440. SetLength(fPerTable[Write],TableIndex+1);
  39441. if fPerTable[Write,TableIndex]=nil then
  39442. fPerTable[Write,TableIndex] := TSynMonitorWithSize.Create(
  39443. fServer.Model.TableProps[TableIndex].Props.SQLTableName+RW[Write]);
  39444. st := fPerTable[Write,TableIndex];
  39445. st.FromExternalMicroSeconds(MicroSecondsElapsed);
  39446. st.AddSize(DataSize);
  39447. if fServer.fStatUsage<>nil then
  39448. fServer.fStatUsage.Modified(st,[]);
  39449. finally
  39450. LeaveCriticalSection(fLock);
  39451. end;
  39452. end;
  39453. function TSQLRestServerMonitor.NotifyThreadCount(delta: integer): integer;
  39454. begin
  39455. EnterCriticalSection(fLock);
  39456. try
  39457. inc(fCurrentThreadCount,delta);
  39458. result := fCurrentThreadCount;
  39459. if delta<>0 then
  39460. Changed;
  39461. finally
  39462. LeaveCriticalSection(fLock);
  39463. end;
  39464. end;
  39465. { TSQLMonitorUsage }
  39466. const
  39467. SQLMONITORSHIFT = 16;
  39468. function TSQLMonitorUsage.GetUsageID: integer;
  39469. begin
  39470. result := fID shr SQLMONITORSHIFT;
  39471. end;
  39472. procedure TSQLMonitorUsage.SetUsageID(Value: integer);
  39473. begin
  39474. fID := (Int64(Value) shl SQLMONITORSHIFT) or Int64(fProcess);
  39475. end;
  39476. { TSynMonitorUsageRest }
  39477. constructor TSynMonitorUsageRest.Create(aStorage: TSQLRest;
  39478. aProcessID: TSynUniqueIdentifierProcess; aStoredClass: TSQLMonitorUsageClass);
  39479. var g: TSynMonitorUsageGranularity;
  39480. begin
  39481. if aStorage=nil then
  39482. raise ESynException.CreateUTF8('%.Create(nil)',[self]);
  39483. if aStoredClass=nil then
  39484. fStoredClass := TSQLMonitorUsage else
  39485. fStoredClass := aStoredClass;
  39486. fStorage := aStorage;
  39487. for g := low(fStoredCache) to high(fStoredCache) do
  39488. fStoredCache[g] := fStoredClass.Create;
  39489. fProcessID := aProcessID;
  39490. {$ifdef WITHLOG}
  39491. fLog := fStorage.LogFamily;
  39492. {$endif}
  39493. inherited Create;
  39494. end;
  39495. destructor TSynMonitorUsageRest.Destroy;
  39496. var g: TSynMonitorUsageGranularity;
  39497. begin
  39498. inherited Destroy; // would save pending changes
  39499. for g := low(fStoredCache) to high(fStoredCache) do
  39500. fStoredCache[g].Free;
  39501. end;
  39502. function TSynMonitorUsageRest.LoadDB(ID: integer; Gran: TSynMonitorUsageGranularity;
  39503. out Track: variant): boolean;
  39504. var recid: TID;
  39505. rec: TSQLMonitorUsage;
  39506. begin
  39507. if (ID=0) or (Gran<Low(fStoredCache)) then begin
  39508. result := false;
  39509. exit;
  39510. end;
  39511. rec := fStoredCache[Gran];
  39512. recid := (Int64(ID) shl SQLMONITORSHIFT) or Int64(fProcessID);
  39513. if rec.IDValue=recid then
  39514. result := true else
  39515. if fStorage.Retrieve(recid,rec) then begin // may use REST cache
  39516. Track := rec.Info;
  39517. if rec.Gran=mugHour then
  39518. fComment := rec.Comment;
  39519. if rec.Process<>fProcessID then
  39520. fLog.SynLog.Log(sllWarning,'%.LoadDB(%,%) received Process=%, expected %',
  39521. [ClassType,ID,ToText(Gran)^,rec.Process,fProcessID]);
  39522. result := true;
  39523. end else begin
  39524. rec.ClearProperties;
  39525. result := false;
  39526. end;
  39527. end;
  39528. function TSynMonitorUsageRest.SaveDB(ID: integer; const Track: variant;
  39529. Gran: TSynMonitorUsageGranularity): boolean;
  39530. var update: boolean;
  39531. recid: TID;
  39532. rec: TSQLMonitorUsage;
  39533. begin
  39534. if (ID=0) or (Gran<Low(fStoredCache)) then begin
  39535. result := false;
  39536. exit;
  39537. end;
  39538. rec := fStoredCache[Gran];
  39539. recid := (Int64(ID) shl SQLMONITORSHIFT) or Int64(fProcessID);
  39540. if rec.IDValue=recid then // already available
  39541. update := true else begin
  39542. update := fStorage.Retrieve(recid,rec); // may use REST cache
  39543. rec.IDValue := recid;
  39544. end;
  39545. rec.Gran := Gran;
  39546. rec.Process := fProcessID;
  39547. if Gran=mugHour then
  39548. rec.Comment := fComment;
  39549. rec.Info := Track;
  39550. if update then
  39551. result := fStorage.Update(rec) else
  39552. result := fStorage.Add(rec,true,true)=recid;
  39553. end;
  39554. { TSQLRestServerURI }
  39555. function TSQLRestServerURI.GetURI: TSQLRestServerURIString;
  39556. begin
  39557. result := Address;
  39558. if Port<>'' then
  39559. result := result+':'+Port;
  39560. if Root<>'' then
  39561. result := result+'/'+Root;
  39562. end;
  39563. procedure TSQLRestServerURI.SetURI(const Value: TSQLRestServerURIString);
  39564. begin
  39565. Split(Value,':',Address,Port);
  39566. if Port<>'' then
  39567. Split(Port,'/',Port,Root) else
  39568. Split(Address,'/',Address,Root);
  39569. end;
  39570. function TSQLRestServerURI.Equals(const other: TSQLRestServerURI): boolean;
  39571. begin
  39572. result := IdemPropNameU(Address,other.Address) and
  39573. IdemPropNameU(Port,other.Port) and
  39574. IdemPropNameU(Root,other.Root);
  39575. end;
  39576. { TServicesPublishedInterfacesList }
  39577. constructor TServicesPublishedInterfacesList.Create(aTimeoutMS: integer);
  39578. begin
  39579. inherited Create;
  39580. fTimeOut := aTimeoutMS;
  39581. fDynArray.Init(TypeInfo(TServicesPublishedInterfacesDynArray),List,@Count);
  39582. fDynArrayTimeoutTix.Init(TypeInfo(TInt64DynArray),fTimeoutTix,@fTimeoutTixCount);
  39583. end;
  39584. function TServicesPublishedInterfacesList.FindURI(
  39585. const aPublicURI: TSQLRestServerURI): integer;
  39586. var tix: Int64;
  39587. begin
  39588. tix := GetTickCount64;
  39589. Safe.Lock;
  39590. try
  39591. for result := 0 to Count-1 do
  39592. if List[result].PublicURI.Equals(aPublicURI) then
  39593. if (fTimeOut=0) or (fTimeoutTix[result]<tix) then
  39594. exit;
  39595. result := -1;
  39596. finally
  39597. Safe.UnLock;
  39598. end;
  39599. end;
  39600. function TServicesPublishedInterfacesList.FindService(
  39601. const aServiceName: RawUTF8): TSQLRestServerURIDynArray;
  39602. var i,n: integer;
  39603. tix: Int64;
  39604. begin
  39605. tix := GetTickCount64;
  39606. result := nil;
  39607. Safe.Lock;
  39608. try
  39609. n := 0;
  39610. for i := Count-1 downto 0 do // downwards to return the latest first
  39611. if FindRawUTF8(List[i].Names,length(List[i].Names),aServiceName,true)>=0 then
  39612. if (fTimeOut=0) or (fTimeoutTix[i]<tix) then begin
  39613. SetLength(result,n+1);
  39614. result[n] := List[i].PublicURI;
  39615. inc(n);
  39616. end;
  39617. finally
  39618. Safe.UnLock;
  39619. end;
  39620. end;
  39621. function TServicesPublishedInterfacesList.FindServiceAll(
  39622. const aServiceName: RawUTF8): TSQLRestServerURIStringDynArray;
  39623. var i,n: integer;
  39624. tix: Int64;
  39625. begin
  39626. tix := GetTickCount64;
  39627. result := nil;
  39628. n := 0;
  39629. Safe.Lock;
  39630. try
  39631. for i := Count-1 downto 0 do // downwards to return the latest first
  39632. if FindRawUTF8(List[i].Names,length(List[i].Names),aServiceName,true)>=0 then
  39633. if (fTimeOut=0) or (fTimeoutTix[i]<tix) then
  39634. AddRawUTF8(TRawUTF8DynArray(result),n,List[i].PublicURI.URI);
  39635. finally
  39636. Safe.UnLock;
  39637. end;
  39638. SetLength(result,n);
  39639. end;
  39640. procedure TServicesPublishedInterfacesList.FindServiceAll(
  39641. const aServiceName: RawUTF8; aWriter: TTextWriter);
  39642. var i: integer;
  39643. tix: Int64;
  39644. begin
  39645. tix := GetTickCount64;
  39646. Safe.Lock;
  39647. try
  39648. aWriter.Add('[');
  39649. if aServiceName='*' then begin
  39650. // for RegisterFromServer: return all TServicesPublishedInterfaces
  39651. for i := 0 to Count-1 do
  39652. with List[i] do
  39653. if (fTimeOut=0) or (fTimeoutTix[i]<tix) then begin
  39654. aWriter.AddRecordJSON(List[i],TypeInfo(TServicesPublishedInterfaces));
  39655. aWriter.Add(',');
  39656. end;
  39657. end else // from SQLRestClientURI.ServiceRetrieveAssociated
  39658. // search matching (and non deprecated) services as TSQLRestServerURI
  39659. for i := Count-1 downto 0 do // downwards to return the latest first
  39660. with List[i] do
  39661. if FindRawUTF8(Names,length(Names),aServiceName,true)>=0 then
  39662. if (fTimeOut=0) or (fTimeoutTix[i]<tix) then begin
  39663. aWriter.AddRecordJSON(PublicURI,TypeInfo(TSQLRestServerURI));
  39664. aWriter.Add(',');
  39665. end;
  39666. aWriter.CancelLastComma;
  39667. aWriter.Add(']');
  39668. finally
  39669. Safe.UnLock;
  39670. end;
  39671. end;
  39672. function TServicesPublishedInterfacesList.RegisterFromServer(Client: TSQLRestClientURI): boolean;
  39673. var json: RawUTF8;
  39674. begin
  39675. result := Client.CallBackGet('stat',['findservice','*'],json)=HTML_SUCCESS;
  39676. if result and (json<>'') then
  39677. RegisterFromServerJSON(json);
  39678. end;
  39679. procedure TServicesPublishedInterfacesList.RegisterFromServerJSON(
  39680. var PublishedJson: RawUTF8);
  39681. var tix: Int64;
  39682. i: integer;
  39683. begin
  39684. Safe.Lock;
  39685. try
  39686. fDynArray.LoadFromJSON(pointer(PublishedJson));
  39687. fDynArrayTimeoutTix.Count := Count;
  39688. tix := GetTickCount64;
  39689. if fTimeout=0 then
  39690. inc(tix,maxInt) else
  39691. inc(tix,fTimeout);
  39692. for i := 0 to Count-1 do
  39693. fTimeoutTix[i] := tix;
  39694. finally
  39695. Safe.UnLock;
  39696. end;
  39697. end;
  39698. procedure TServicesPublishedInterfacesList.RegisterFromClientJSON(
  39699. var PublishedJson: RawUTF8);
  39700. var i: integer;
  39701. nfo: TServicesPublishedInterfaces;
  39702. crc: cardinal;
  39703. tix: Int64;
  39704. P: PUTF8Char;
  39705. begin
  39706. if PublishedJson='' then
  39707. exit;
  39708. crc := crc32c(0,pointer(PublishedJson),length(PublishedJson));
  39709. if (self=nil) or ((fLastPublishedJson<>0) and (crc=fLastPublishedJson)) then
  39710. exit; // rough but working good in practice, when similar _contract_
  39711. P := Pointer(PublishedJson);
  39712. if P^='[' then
  39713. inc(P); // when transmitted as [params] in a _contract_ HTTP body content
  39714. if (RecordLoadJSON(nfo,P,TypeInfo(TServicesPublishedInterfaces))=nil) or
  39715. (nfo.PublicURI.Address='') then
  39716. exit; // invalid supplied JSON content
  39717. Safe.Lock;
  39718. try // store so that the latest updated version is always at the end
  39719. for i := 0 to Count-1 do
  39720. if List[i].PublicURI.Equals(nfo.PublicURI) then begin // ignore Timeout
  39721. fDynArray.Delete(i);
  39722. fDynArrayTimeoutTix.Delete(i);
  39723. break;
  39724. end;
  39725. if nfo.Names<>nil then begin
  39726. fDynArray.Add(nfo);
  39727. tix := GetTickCount64;
  39728. if fTimeout=0 then
  39729. inc(tix,maxInt) else
  39730. inc(tix,fTimeout);
  39731. fDynArrayTimeoutTix.Add(tix);
  39732. end;
  39733. fLastPublishedJson := crc;
  39734. finally
  39735. Safe.UnLock;
  39736. end;
  39737. end;
  39738. { TSQLRestStorageRecordBased }
  39739. function TSQLRestStorageRecordBased.EngineAdd(TableModelIndex: integer;
  39740. const SentData: RawUTF8): TID;
  39741. var Rec: TSQLRecord;
  39742. begin
  39743. result := 0; // mark error
  39744. if (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
  39745. exit;
  39746. Rec := fStoredClass.Create;
  39747. try
  39748. Rec.FillFrom(SentData);
  39749. StorageLock(true);
  39750. try
  39751. result := AddOne(Rec,Rec.fID>0,SentData);
  39752. finally
  39753. StorageUnLock;
  39754. end;
  39755. finally
  39756. if result<=0 then
  39757. Rec.Free; // on success, Rec is owned by fValue: TObjectList
  39758. end;
  39759. end;
  39760. function TSQLRestStorageRecordBased.EngineUpdate(TableModelIndex: integer; ID: TID;
  39761. const SentData: RawUTF8): boolean;
  39762. var Rec: TSQLRecord;
  39763. begin
  39764. // this implementation won't handle partial fields update (e.g. BatchUpdate
  39765. // after FillPrepare) - but TSQLRestStorageInMemory.EngineUpdate will
  39766. if (ID<=0) or (TableModelIndex<0) or
  39767. (Model.Tables[TableModelIndex]<>fStoredClass) then begin
  39768. result := false; // mark error
  39769. exit;
  39770. end;
  39771. StorageLock(true);
  39772. try
  39773. Rec := fStoredClass.Create;
  39774. try
  39775. Rec.FillFrom(SentData);
  39776. Rec.fID := ID;
  39777. result := UpdateOne(Rec,SentData);
  39778. finally
  39779. Rec.Free;
  39780. end;
  39781. finally
  39782. StorageUnLock;
  39783. end;
  39784. end;
  39785. function TSQLRestStorageRecordBased.UpdateOne(ID: TID;
  39786. const Values: TSQLVarDynArray): boolean;
  39787. var Rec: TSQLRecord;
  39788. begin
  39789. if (ID<=0) then begin
  39790. result := false; // mark error
  39791. exit;
  39792. end;
  39793. StorageLock(true);
  39794. try
  39795. Rec := fStoredClass.Create;
  39796. try
  39797. Rec.SetFieldSQLVars(Values);
  39798. Rec.fID := ID;
  39799. result := UpdateOne(Rec,Rec.GetJSONValues(true,False,soUpdate));
  39800. finally
  39801. Rec.Free;
  39802. end;
  39803. finally
  39804. StorageUnLock;
  39805. end;
  39806. end;
  39807. { TSQLRestStorageInMemory }
  39808. constructor TSQLRestStorageInMemory.Create(aClass: TSQLRecordClass; aServer: TSQLRestServer;
  39809. const aFileName: TFileName = ''; aBinaryFile: boolean=false);
  39810. var F: integer;
  39811. begin
  39812. inherited Create(aClass,aServer);
  39813. if (fStoredClassProps<>nil) and (fStoredClassProps.Kind in INSERT_WITH_ID) then
  39814. raise EModelException.CreateUTF8('%.Create: % virtual table can''t be static',
  39815. [self,aClass]);
  39816. fFileName := aFileName;
  39817. fBinaryFile := aBinaryFile;
  39818. fValue := TObjectList.Create;
  39819. fSearchRec := fStoredClass.Create;
  39820. fIDSorted := true; // sorted by design of this class (may change in children)
  39821. if (ClassType<>TSQLRestStorageInMemory) and (fStoredClassProps<>nil) then
  39822. with fStoredClassProps do begin // used by AdaptSQLForEngineList() method
  39823. fBasicUpperSQLSelect[false] := SynCommons.UpperCase(SQL.SelectAllWithRowID);
  39824. SetLength(fBasicUpperSQLSelect[false],length(fBasicUpperSQLSelect[false])-1); // trim right ';'
  39825. fBasicUpperSQLSelect[true] := StringReplaceAll(fBasicUpperSQLSelect[false],' ROWID,',' ID,');
  39826. end;
  39827. if not IsZero(fIsUnique) then begin
  39828. fUniqueFields := TObjectList.Create;
  39829. with fStoredClassRecordProps do
  39830. for F := 0 to Fields.Count-1 do
  39831. if F in fIsUnique then
  39832. // CaseInsensitive=true just like in SQlite3 (but slower)
  39833. fUniqueFields.Add(TListFieldHash.Create(fValue,Fields.List[F],true));
  39834. end;
  39835. ReloadFromFile;
  39836. end;
  39837. function TSQLRecordCompare(Item1,Item2: Pointer): integer;
  39838. var tmp: Int64;
  39839. begin // we assume Item1<>nil and Item2<>nil in fValue[]
  39840. tmp := TSQLRecord(Item1).fID-TSQLRecord(Item2).fID;
  39841. if tmp<0 then
  39842. result := -1 else
  39843. if tmp>0 then
  39844. result := 1 else
  39845. result := 0;
  39846. end;
  39847. function TSQLRestStorageInMemory.AddOne(Rec: TSQLRecord; ForceID: boolean;
  39848. const SentData: RawUTF8): TID;
  39849. var ndx,i: integer;
  39850. lastID: TID;
  39851. needSort: boolean;
  39852. hash: TListFieldHash;
  39853. begin
  39854. if (self=nil) or (Rec=nil) then begin
  39855. result := -1; // mark error
  39856. exit;
  39857. end;
  39858. if fValue.Count=0 then
  39859. lastID := 0 else // default ID for a void table
  39860. lastID := TSQLRecord(fValue[fValue.Count-1]).fID; // ID in increasing order
  39861. needSort := false;
  39862. if ForceID then begin // check forced ID
  39863. if Rec.fID<=0 then
  39864. raise EORMException.CreateUTF8('%.AddOne(%.ForceID=0)',[self,Rec]);
  39865. if Rec.fID<=lastID then begin
  39866. if fUniqueFields<>nil then begin
  39867. for i := 0 to fUniqueFields.Count-1 do begin
  39868. hash := fUniqueFields.List[i];
  39869. ndx := hash.Scan(Rec,fValue.Count); // O(n) search to avoid hashing
  39870. if ndx>=0 then begin
  39871. InternalLog('%.AddOne: Duplicated field "%" value for % and %',
  39872. [ClassType,hash.Field.Name,Rec,TSQLRecord(fValue.List[ndx])],sllTrace);
  39873. result := 0; // duplicate unique fields -> error
  39874. exit;
  39875. end;
  39876. hash.Invalidate;
  39877. end;
  39878. InternalLog('%.AddOne(%.ForceID=%<=lastID=%) -> UniqueFields[].Invalidate',
  39879. [ClassType,Rec.ClassType,Rec.fID,lastID],sllTrace);
  39880. end;
  39881. if IDToIndex(Rec.fID)>=0 then
  39882. raise EORMException.CreateUTF8('%.AddOne(%.ForceID=%) already existing',
  39883. [self,Rec,Rec.fID]);
  39884. needSort := true; // brutal, but working
  39885. end;
  39886. result := Rec.fID;
  39887. end else begin // not ForceID -> compute new ID
  39888. result := lastID+1;
  39889. Rec.fID := result;
  39890. end;
  39891. ndx := fValue.Add(Rec);
  39892. if needSort then
  39893. fValue.Sort(TSQLRecordCompare) else // fUniqueFields[] already checked
  39894. if fUniqueFields<>nil then
  39895. for i := 0 to fUniqueFields.Count-1 do // perform hash of List[Count-1]
  39896. if not TListFieldHash(fUniqueFields.List[i]).EnsureJustAddedNotDuplicated then begin
  39897. InternalLog('%.AddOne: Duplicated field "%" value for %',
  39898. [ClassType,TListFieldHash(fUniqueFields.List[i]).Field.Name,Rec],sllTrace);
  39899. result := 0; // duplicate unique fields -> error
  39900. fValue.List[ndx] := nil; // avoid GPF within Delete()
  39901. fValue.Delete(ndx);
  39902. exit;
  39903. end;
  39904. fModified := true;
  39905. if Owner<>nil then
  39906. Owner.InternalUpdateEvent(seAdd,fStoredClassProps.TableIndex,result,SentData,nil);
  39907. end;
  39908. function TSQLRestStorageInMemory.UniqueFieldsUpdateOK(aRec: TSQLRecord; aUpdateIndex: integer): boolean;
  39909. var i,ndx: integer;
  39910. begin
  39911. if fUniqueFields<>nil then begin
  39912. result := false;
  39913. with fUniqueFields do
  39914. for i := 0 to Count-1 do begin
  39915. ndx := TListFieldHash(List[i]).Find(aRec);
  39916. if (ndx>=0) and (ndx<>aUpdateIndex) then
  39917. exit; // duplicate value found at another entry
  39918. end;
  39919. end;
  39920. result := true;
  39921. end;
  39922. function TSQLRestStorageInMemory.UniqueFieldHash(aFieldIndex: integer): TListFieldHash;
  39923. var i: integer;
  39924. begin
  39925. if (fUniqueFields<>nil) and
  39926. (cardinal(aFieldIndex)<cardinal(fStoredClassRecordProps.Fields.Count)) then
  39927. with fUniqueFields do
  39928. for i := 0 to Count-1 do begin
  39929. result := List[i];
  39930. if result.FieldIndex=aFieldIndex then
  39931. exit;
  39932. end;
  39933. result := nil;
  39934. end;
  39935. function TSQLRestStorageInMemory.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
  39936. begin
  39937. if (self=nil) or (ID<=0) or (TableModelIndex<0) or
  39938. (Model.Tables[TableModelIndex]<>fStoredClass) then
  39939. result := false else begin
  39940. StorageLock(True);
  39941. try
  39942. result := DeleteOne(IDToIndex(ID));
  39943. finally
  39944. StorageUnLock;
  39945. end;
  39946. end;
  39947. end;
  39948. function TSQLRestStorageInMemory.DeleteOne(aIndex: integer): boolean;
  39949. var F: integer;
  39950. begin
  39951. if cardinal(aIndex)>=cardinal(fValue.Count) then
  39952. result := false else begin
  39953. if fUniqueFields<>nil then
  39954. for F := 0 to fUniqueFields.Count-1 do
  39955. TListFieldHash(fUniqueFields.List[F]).Invalidate;
  39956. if Owner<>nil then // notify BEFORE deletion
  39957. Owner.InternalUpdateEvent(seDelete,fStoredClassProps.TableIndex,
  39958. TSQLRecord(fValue.List[aIndex]).fID,'',nil);
  39959. fValue.Delete(aIndex); // TObjectList.Delete() will Free record
  39960. fModified := true;
  39961. result := true;
  39962. end;
  39963. end;
  39964. function TSQLRestStorageInMemory.EngineDeleteWhere(TableModelIndex: Integer;
  39965. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  39966. var ndx: TIntegerDynArray;
  39967. n,i: integer;
  39968. begin // RecordCanBeUpdated() has already been called
  39969. result := false;
  39970. n := length(IDs);
  39971. SetLength(ndx,n);
  39972. dec(n);
  39973. StorageLock(True);
  39974. try
  39975. for i := 0 to n do begin
  39976. ndx[i] := IDToIndex(IDs[i]);
  39977. if ndx[i]<0 then
  39978. exit;
  39979. end;
  39980. if fUniqueFields<>nil then
  39981. for i := 0 to fUniqueFields.Count-1 do
  39982. TListFieldHash(fUniqueFields.List[i]).Invalidate;
  39983. if Owner<>nil then
  39984. for i := 0 to n do
  39985. Owner.InternalUpdateEvent(seDelete,fStoredClassProps.TableIndex,IDs[i],'',nil); // notify BEFORE deletion
  39986. QuickSortInteger(pointer(ndx),0,n); // deletion a bit faster in reverse order
  39987. for i := n downto 0 do
  39988. fValue.Delete(ndx[i]);
  39989. fModified := true;
  39990. result := true;
  39991. finally
  39992. StorageUnLock;
  39993. end;
  39994. end;
  39995. function TSQLRestStorageInMemory.EngineExecute(const aSQL: RawUTF8): boolean;
  39996. begin
  39997. result := false; // there is no SQL engine with this class
  39998. end;
  39999. destructor TSQLRestStorageInMemory.Destroy;
  40000. begin
  40001. UpdateFile;
  40002. fValue.Free; // TObjectList.Destroy will free all stored TSQLRecord instances
  40003. fUniqueFields.Free;
  40004. fSearchRec.Free;
  40005. inherited Destroy;
  40006. end;
  40007. function TSQLRestStorageInMemory.GetCount: integer;
  40008. begin
  40009. if Self<>nil then
  40010. result := fValue.Count else
  40011. result := 0;
  40012. end;
  40013. function TSQLRestStorageInMemory.GetID(Index: integer): TID;
  40014. begin
  40015. with fValue do
  40016. if (self=nil) or (cardinal(Index)>=cardinal(Count)) then
  40017. result := 0 else
  40018. result := TSQLRecord(List[Index]).fID;
  40019. end;
  40020. function TSQLRestStorageInMemory.GetItem(Index: integer): TSQLRecord;
  40021. begin
  40022. if self<>nil then
  40023. with fValue do
  40024. if cardinal(Index)>=cardinal(Count) then
  40025. raise EORMException.CreateUTF8('%.GetItem(%) out of range',[self,Index]) else
  40026. result := List[Index] else
  40027. result := nil;
  40028. end;
  40029. function TSQLRestStorageInMemory.GetListPtr: PPointerArray;
  40030. begin
  40031. result := pointer(fValue.List);
  40032. end;
  40033. procedure TSQLRestStorageInMemory.GetJSONValuesEvent(aDest: pointer;
  40034. aRec: TSQLRecord; aIndex: integer);
  40035. var W: TJSONSerializer absolute aDest;
  40036. begin
  40037. aRec.GetJSONValues(W);
  40038. W.Add(',');
  40039. end;
  40040. procedure TSQLRestStorageInMemory.AddIntegerDynArrayEvent(
  40041. aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  40042. var Ints: TList absolute aDest;
  40043. begin
  40044. Ints.Add(pointer(aIndex));
  40045. end;
  40046. procedure TSQLRestStorageInMemory.DoNothingEvent(
  40047. aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  40048. begin
  40049. end;
  40050. function TSQLRestStorageInMemory.AdaptSQLForEngineList(var SQL: RawUTF8): boolean;
  40051. var P: PUTF8Char;
  40052. Prop: RawUTF8;
  40053. WithoutRowID: boolean;
  40054. begin
  40055. result := inherited AdaptSQLForEngineList(SQL);
  40056. if result then
  40057. exit; // 'select * from table'
  40058. if IdemPropNameU(fBasicSQLCount,SQL) or
  40059. IdemPropNameU(fBasicSQLHasRows[false],SQL) or
  40060. IdemPropNameU(fBasicSQLHasRows[true],SQL) then begin
  40061. result := true;
  40062. exit; // 'select count(*) from table' will be handled as static
  40063. end;
  40064. if fBasicUpperSQLSelect[false]='' then
  40065. exit;
  40066. if IdemPChar(pointer(SQL),pointer(fBasicUpperSQLSelect[false])) then
  40067. WithoutRowID := false else
  40068. if IdemPChar(pointer(SQL),pointer(fBasicUpperSQLSelect[true])) then
  40069. WithoutRowID := true else
  40070. exit;
  40071. P := pointer(SQL);
  40072. inc(P,length(fBasicUpperSQLSelect[WithoutRowID]));
  40073. if P^ in [#0,';'] then begin
  40074. result := true; // properly ended the WHERE clause as 'SELECT * FROM table'
  40075. exit;
  40076. end;
  40077. P := GotoNextNotSpace(P);
  40078. if not IdemPChar(P,'WHERE ') then begin
  40079. if IdemPChar(P,'LIMIT ') then
  40080. result := true;
  40081. exit;
  40082. end;
  40083. P := GotoNextNotSpace(P+6);
  40084. Prop := GetNextItem(P,'=');
  40085. if (P=nil) or (fStoredClassRecordProps.Fields.IndexByName(Prop)<0) then
  40086. exit;
  40087. if PWord(P)^=ord(':')+ord('(') shl 8 then
  40088. inc(P,2); // +2 to ignore :(...): parameter
  40089. if P^ in ['''','"'] then begin
  40090. P := GotoEndOfQuotedString(P);
  40091. if not (P^ in ['''','"']) then
  40092. exit;
  40093. end;
  40094. repeat inc(P) until P^ in [#0..' ',';',')']; // go to end of value
  40095. if PWord(P)^=ord(')')+ord(':')shl 8 then
  40096. inc(P,2); // ignore :(...): parameter
  40097. P := GotoNextNotSpace(P);
  40098. if (P^ in [#0,';']) or IdemPChar(P,'LIMIT ') then
  40099. result := true; // properly ended the WHERE clause as 'FIELDNAME=value'
  40100. end;
  40101. function TSQLRestStorageInMemory.FindWhereEqual(const WhereFieldName, WhereValue: RawUTF8;
  40102. OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: integer;
  40103. CaseInsensitive: boolean): PtrInt;
  40104. var WhereFieldIndex: integer;
  40105. begin
  40106. result := 0;
  40107. if (Self=nil) or not Assigned(OnFind) then
  40108. exit;
  40109. if IsRowID(pointer(WhereFieldName)) then
  40110. WhereFieldIndex := 0 else begin
  40111. WhereFieldIndex := fStoredClassRecordProps.Fields.IndexByName(pointer(WhereFieldName));
  40112. if WhereFieldIndex<0 then
  40113. exit;
  40114. inc(WhereFieldIndex); // FindWhereEqual() expects index = RTTI+1
  40115. end;
  40116. result := FindWhereEqual(WhereFieldIndex,WhereValue,Onfind,Dest,
  40117. FoundLimit,FoundOffset,CaseInsensitive);
  40118. end;
  40119. function TSQLRestStorageInMemory.FindWhereEqual(WhereField: integer;
  40120. const WhereValue: RawUTF8; OnFind: TFindWhereEqualEvent; Dest: pointer;
  40121. FoundLimit,FoundOffset: integer; CaseInsensitive: boolean): PtrInt;
  40122. var i, ndx, i32: integer;
  40123. i64: Int64;
  40124. err, currentRow: integer;
  40125. P: TSQLPropInfo;
  40126. nfo: PPropInfo;
  40127. Hash: TListFieldHash;
  40128. offs: PtrUInt;
  40129. item: PPointer;
  40130. procedure FoundOne;
  40131. begin
  40132. if FoundOffset>0 then begin // omit first FoundOffset rows
  40133. inc(currentRow);
  40134. if currentRow>FoundOffset then
  40135. FoundOffset := 0 else
  40136. exit;
  40137. end;
  40138. if Assigned(OnFind) then
  40139. OnFind(Dest,TSQLRecord(item^),(PtrUInt(item)-PtrUInt(fValue.List)) shr POINTERSHR);
  40140. inc(result);
  40141. end;
  40142. begin
  40143. result := 0;
  40144. if fValue.Count=0 then
  40145. exit;
  40146. if FoundLimit<=0 then
  40147. FoundLimit := maxInt;
  40148. if WhereField=SYNTABLESTATEMENTWHEREID then begin
  40149. if FoundOffset<=0 then begin // omit first FoundOffset rows
  40150. i64 := GetInt64(pointer(WhereValue),err);
  40151. if (err=0) and (i64>0) then begin
  40152. ndx := IDToIndex(i64); // use fast binary search
  40153. if ndx>=0 then begin
  40154. if Assigned(OnFind) then
  40155. OnFind(Dest,TSQLRecord(fValue.List[ndx]),ndx);
  40156. inc(result);
  40157. end;
  40158. end;
  40159. end;
  40160. exit;
  40161. end else
  40162. if cardinal(WhereField)>cardinal(fStoredClassRecordProps.Fields.Count) then
  40163. exit;
  40164. dec(WhereField); // WHERE WhereField=WhereValue (WhereField=RTTIfield+1)
  40165. P := fStoredClassRecordProps.Fields.List[WhereField];
  40166. if not (P.SQLFieldType in COPIABLE_FIELDS) then
  40167. exit; // nothing to search (e.g. sftUnknown or sftMany)
  40168. // use fUniqueFields[] hash array for O(1) search if available
  40169. Hash := UniqueFieldHash(WhereField);
  40170. if Hash<>nil then begin
  40171. if FoundOffset<=0 then begin // omit first FoundOffset rows, for ID unique field
  40172. P.SetValueVar(fSearchRec,WhereValue,false);
  40173. ndx := Hash.Find(fSearchRec);
  40174. if ndx>=0 then begin
  40175. if Assigned(OnFind) then
  40176. OnFind(Dest,fValue.List[ndx],ndx);
  40177. inc(result);
  40178. end;
  40179. end;
  40180. exit;
  40181. end;
  40182. // full scan optimized search for a specified value
  40183. currentRow := 0;
  40184. item := pointer(fValue.List);
  40185. if P.InheritsFrom(TSQLPropInfoRTTIInt32) then begin
  40186. // optimized search for 8/16/32-bit Integer values
  40187. i32 := GetInteger(pointer(WhereValue),err);
  40188. if err<>0 then
  40189. exit;
  40190. nfo := TSQLPropInfoRTTI(P).PropInfo;
  40191. offs := TSQLPropInfoRTTI(P).fGetterIsFieldPropOffset;
  40192. if (offs<>0) and {$ifndef CPU64}(nfo^.PropType^.Kind=tkClass) or{$endif}
  40193. ((nfo^.PropType^.Kind=tkInteger)and(nfo^.PropType^.OrdType=otSLong)) then begin
  40194. // optimized version for fast retrieval of signed 32-bit Integer field value
  40195. for i := 1 to fValue.Count do begin
  40196. if PInteger(PPtrUInt(item)^+offs)^=i32 then begin
  40197. FoundOne;
  40198. if result>=FoundLimit then
  40199. exit;
  40200. end;
  40201. inc(item);
  40202. end;
  40203. end else
  40204. // 8-bit or 16-bit value, or there is a getter procedure -> use GetOrdProp()
  40205. for i := 1 to fValue.Count do begin
  40206. if nfo^.GetOrdProp(item^)=i32 then begin
  40207. FoundOne;
  40208. if result>=FoundLimit then
  40209. exit;
  40210. end;
  40211. inc(item);
  40212. end;
  40213. end else
  40214. if P.InheritsFrom(TSQLPropInfoRTTIInt64) then begin
  40215. // stored as one 64-bit Integer value -> optimized search
  40216. i64 := GetInt64(pointer(WhereValue),err);
  40217. if err<>0 then
  40218. exit;
  40219. nfo := TSQLPropInfoRTTI(P).PropInfo;
  40220. offs := TSQLPropInfoRTTI(P).fGetterIsFieldPropOffset;
  40221. if offs<>0 then begin
  40222. for i := 1 to fValue.Count do begin
  40223. if PInt64(PPtrUInt(item)^+offs)^=i64 then begin
  40224. FoundOne;
  40225. if result>=FoundLimit then
  40226. exit;
  40227. end;
  40228. inc(item);
  40229. end;
  40230. end else
  40231. for i := 1 to fValue.Count do begin
  40232. if nfo^.GetInt64Prop(item^)=i64 then begin
  40233. FoundOne;
  40234. if result>=FoundLimit then
  40235. exit;
  40236. end;
  40237. inc(item);
  40238. end;
  40239. end else begin
  40240. // generic search of any value, using fast CompareValue() overridden method
  40241. P.SetValueVar(fSearchRec,WhereValue,false);
  40242. for i := 1 to fValue.Count do begin
  40243. if P.CompareValue(item^,fSearchRec,CaseInsensitive)=0 then begin
  40244. FoundOne;
  40245. if result>=FoundLimit then
  40246. exit;
  40247. end;
  40248. inc(item);
  40249. end;
  40250. end;
  40251. end;
  40252. function TSQLRestStorageInMemory.FindMax(WhereField: integer; out max: Int64): boolean;
  40253. var list: PPointerArray;
  40254. P: TSQLPropInfo;
  40255. nfo: PPropInfo;
  40256. i: integer;
  40257. v: Int64;
  40258. begin
  40259. result := false;
  40260. max := low(Int64);
  40261. if fValue.Count=0 then
  40262. exit;
  40263. list := pointer(fValue.List);
  40264. if WhereField=SYNTABLESTATEMENTWHEREID then begin
  40265. max := TSQLRecord(list[fValue.Count-1]).IDValue; // should be ordered
  40266. result := true;
  40267. exit;
  40268. end;
  40269. if cardinal(WhereField)>cardinal(fStoredClassRecordProps.Fields.Count) then
  40270. exit;
  40271. dec(WhereField); // WHERE WhereField=WhereValue (WhereField=RTTIfield+1)
  40272. P := fStoredClassRecordProps.Fields.List[WhereField];
  40273. if P.InheritsFrom(TSQLPropInfoRTTIInt32) then begin
  40274. nfo := TSQLPropInfoRTTI(P).PropInfo;
  40275. for i := 0 to fValue.Count-1 do begin
  40276. v := nfo.GetOrdProp(list[i]);
  40277. if v>max then
  40278. max := v;
  40279. end;
  40280. result := true;
  40281. end
  40282. else if P.InheritsFrom(TSQLPropInfoRTTIInt64) then begin
  40283. nfo := TSQLPropInfoRTTI(P).PropInfo;
  40284. for i := 0 to fValue.Count-1 do begin
  40285. v := nfo.GetInt64Prop(list[i]);
  40286. if v>max then
  40287. max := v;
  40288. end;
  40289. result := true;
  40290. end;
  40291. end;
  40292. procedure TSQLRestStorageInMemory.ForEach(WillModifyContent: boolean;
  40293. OnEachProcess: TFindWhereEqualEvent; Dest: pointer);
  40294. var i: integer;
  40295. begin
  40296. if (self=nil) or (fValue.Count=0) or not Assigned(OnEachProcess) then
  40297. exit;
  40298. StorageLock(WillModifyContent);
  40299. try
  40300. for i := 0 to fValue.Count-1 do
  40301. OnEachProcess(Dest,fValue.List[i],i);
  40302. finally
  40303. StorageUnLock;
  40304. end;
  40305. end;
  40306. function TSQLRestStorageInMemory.GetJSONValues(Stream: TStream;
  40307. Expand: boolean; Stmt: TSynTableStatement): PtrInt;
  40308. var ndx,KnownRowsCount: integer;
  40309. {$ifndef NOVARIANTS}
  40310. j: integer;
  40311. id: Int64;
  40312. {$endif}
  40313. W: TJSONSerializer;
  40314. IsNull: boolean;
  40315. Prop: TSQLPropInfo;
  40316. bits: TSQLFieldBits;
  40317. withID: boolean;
  40318. label err;
  40319. begin // exact same format as TSQLTable.GetJSONValues()
  40320. result := 0;
  40321. if length(Stmt.Where)>1 then
  40322. raise EORMException.CreateUTF8('%.GetJSONValues on % with Stmt.Where[]=%',
  40323. [self,fStoredClass,length(Stmt.Where)]);
  40324. if Stmt.Where=nil then // no WHERE statement -> get all rows -> set rows count
  40325. if (Stmt.Limit>0) and (fValue.Count>Stmt.Limit) then
  40326. KnownRowsCount := Stmt.Limit else
  40327. KnownRowsCount := fValue.Count else
  40328. KnownRowsCount := 0;
  40329. Stmt.SelectFieldBits(bits,withID);
  40330. W := fStoredClassRecordProps.CreateJSONWriter(Stream,Expand,withID,bits,KnownRowsCount);
  40331. if W<>nil then
  40332. try
  40333. if Expand then
  40334. W.Add('[');
  40335. if Stmt.Where=nil then begin // no WHERE statement -> all rows
  40336. for ndx := 0 to KnownRowsCount-1 do begin
  40337. if Expand then
  40338. W.AddCR; // for better readability
  40339. TSQLRecord(fValue.List[ndx]).GetJSONValues(W);
  40340. W.Add(',');
  40341. end;
  40342. result := KnownRowsCount;
  40343. end else
  40344. case Stmt.Where[0].Operator of
  40345. opEqualTo:
  40346. result := FindWhereEqual(Stmt.Where[0].Field,Stmt.Where[0].Value,
  40347. GetJSONValuesEvent,W,Stmt.Limit,Stmt.Offset);
  40348. {$ifndef NOVARIANTS}
  40349. opIn:
  40350. if (Stmt.Where[0].Field<>0) or // only handle ID IN (..) by now
  40351. (Stmt.Offset>0) then
  40352. goto err else
  40353. with _Safe(Stmt.Where[0].ValueVariant)^ do
  40354. for ndx := 0 to Count-1 do
  40355. if VariantToInt64(Values[ndx],id) then begin
  40356. j := IDToIndex(id);
  40357. if j>=0 then begin
  40358. TSQLRecord(fValue.List[j]).GetJSONValues(W);
  40359. W.Add(',');
  40360. inc(result);
  40361. if (Stmt.Limit>0) and (result>=Stmt.Limit) then
  40362. break;
  40363. end;
  40364. end else
  40365. goto err;
  40366. {$endif}
  40367. opIsNull, opIsNotNull:
  40368. if Stmt.Where[0].Field>0 then begin
  40369. Prop := fStoredClassRecordProps.Fields.List[Stmt.Where[0].Field-1];
  40370. if Prop.InheritsFrom(TSQLPropInfoRTTIRawBlob) then begin
  40371. IsNull := Stmt.Where[0].Operator=opIsNull;
  40372. for ndx := 0 to fValue.Count-1 do
  40373. if TSQLPropInfoRTTIRawBlob(Prop).IsNull(fValue.List[ndx])=IsNull then begin
  40374. TSQLRecord(fValue.List[ndx]).GetJSONValues(W);
  40375. W.Add(',');
  40376. inc(result);
  40377. if (Stmt.Limit>0) and (result>=Stmt.Limit) then
  40378. break;
  40379. end;
  40380. end else
  40381. goto err;
  40382. end else
  40383. goto err;
  40384. else begin
  40385. err: W.CancelAll;
  40386. result := 0;
  40387. exit;
  40388. end;
  40389. end;
  40390. if (result=0) and W.Expand then begin
  40391. // we want the field names at least, even with no data
  40392. W.Expand := false; // {"fieldCount":2,"values":["col1","col2"]}
  40393. W.CancelAll;
  40394. fStoredClassRecordProps.SetJSONWriterColumnNames(W,0);
  40395. end;
  40396. W.EndJSONObject(KnownRowsCount,result);
  40397. finally
  40398. W.Free;
  40399. end;
  40400. end;
  40401. function TSQLRestStorageInMemory.IDToIndex(ID: TID): integer;
  40402. var L, R: integer;
  40403. cmp: TID;
  40404. begin
  40405. if self<>nil then
  40406. with fValue do begin
  40407. R := Count-1;
  40408. if fIDSorted and (R>=8) then begin
  40409. // IDs are sorted -> use fast binary search algorithm
  40410. L := 0;
  40411. repeat
  40412. result := (L + R) shr 1;
  40413. cmp := TSQLRecord(List[result]).fID-ID;
  40414. if cmp=0 then
  40415. exit;
  40416. if cmp<0 then
  40417. L := result + 1 else
  40418. R := result - 1;
  40419. until (L > R);
  40420. end else
  40421. // IDs are not sorted -> compare all from beginning to end
  40422. for result := 0 to R do
  40423. if TSQLRecord(List[result]).fID=ID then
  40424. exit;
  40425. end;
  40426. result := -1;
  40427. end;
  40428. function TSQLRestStorageInMemory.EngineList(const SQL: RawUTF8;
  40429. ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8;
  40430. // - GetJSONValues/FindWhereEqual will handle basic REST commands (not all SQL)
  40431. // only valid SQL command is "SELECT Field1,Field2 FROM Table WHERE ID=120;",
  40432. // i.e one Table SELECT with one optional "WHERE fieldname = value" statement
  40433. // - handle also basic "SELECT Count(*) FROM TableName;" SQL statement
  40434. // Note: this is sufficient for OneFieldValue() and MultiFieldValue() to work
  40435. var MS: TRawByteStringStream;
  40436. ResCount: PtrInt;
  40437. Stmt: TSynTableStatement;
  40438. max: Int64;
  40439. procedure SetCount(aCount: integer);
  40440. begin
  40441. FormatUTF8('[{"Count(*)":%}]'#$A,[aCount],result);
  40442. ResCount := 1;
  40443. end;
  40444. begin
  40445. result := '';
  40446. ResCount := 0;
  40447. StorageLock(false);
  40448. try
  40449. if IdemPropNameU(fBasicSQLCount,SQL) then
  40450. SetCount(TableRowCount(fStoredClass)) else
  40451. if IdemPropNameU(fBasicSQLHasRows[false],SQL) or
  40452. IdemPropNameU(fBasicSQLHasRows[true],SQL) then
  40453. if TableRowCount(fStoredClass)=0 then begin
  40454. result := '{"fieldCount":1,"values":["RowID"]}'#$A;
  40455. ResCount := 0;
  40456. end else begin // return one row with fake ID=1
  40457. result := '[{"RowID":1}]'#$A;
  40458. ResCount := 1;
  40459. end else begin
  40460. Stmt := TSynTableStatement.Create(SQL,
  40461. fStoredClassRecordProps.Fields.IndexByName,
  40462. fStoredClassRecordProps.SimpleFieldsBits[soSelect]);
  40463. try
  40464. if (Stmt.SQLStatement='') or // parsing failed
  40465. (length(Stmt.Where)>1) or // only a SINGLE expression is allowed yet
  40466. not IdemPropNameU(Stmt.TableName,fStoredClassRecordProps.SQLTableName) then
  40467. // invalid request -> return ''
  40468. exit;
  40469. if Stmt.SelectFunctionCount=0 then begin
  40470. // save rows as JSON, with appropriate search according to Where.* arguments
  40471. MS := TRawByteStringStream.Create;
  40472. try
  40473. ForceAJAX := ForceAJAX or not Owner.NoAJAXJSON;
  40474. ResCount := GetJSONValues(MS,ForceAJAX,Stmt);
  40475. result := MS.DataString;
  40476. finally
  40477. MS.Free;
  40478. end;
  40479. end else
  40480. if (length(Stmt.Select)<>1) or (Stmt.SelectFunctionCount<>1) or
  40481. ((Stmt.Limit>1) or (Stmt.Offset<>0)) then
  40482. // handle a single max() or count() function with no LIMIT nor OFFSET
  40483. exit else
  40484. case Stmt.Select[0].FunctionKnown of
  40485. funcCountStar:
  40486. if Stmt.Where=nil then
  40487. // was e.g. "SELECT Count(*) FROM TableName;"
  40488. SetCount(TableRowCount(fStoredClass)) else begin
  40489. // was e.g. "SELECT Count(*) FROM TableName WHERE ..."
  40490. ResCount := FindWhereEqual(Stmt.Where[0].Field,Stmt.Where[0].Value,
  40491. DoNothingEvent,nil,0,0);
  40492. case Stmt.Where[0].Operator of
  40493. opEqualTo: SetCount(ResCount);
  40494. opNotEqualTo: SetCount(TableRowCount(fStoredClass)-ResCount);
  40495. end;
  40496. end;
  40497. funcMax:
  40498. if (Stmt.Where=nil) and FindMax(Stmt.Select[0].Field,max) then begin
  40499. FormatUTF8('[{"Max()":%}]'#$A,[max],result);
  40500. ResCount := 1;
  40501. end;
  40502. else exit;
  40503. end;
  40504. finally
  40505. Stmt.Free;
  40506. end;
  40507. end;
  40508. finally
  40509. StorageUnLock;
  40510. end;
  40511. if ReturnedRowCount<>nil then
  40512. ReturnedRowCount^ := ResCount;
  40513. end;
  40514. procedure TSQLRestStorageInMemory.DropValues;
  40515. begin
  40516. StorageLock(true);
  40517. try
  40518. fModified := fValue.Count>0;
  40519. fValue.Clear;
  40520. UpdateFile;
  40521. finally
  40522. StorageUnLock;
  40523. end;
  40524. end;
  40525. procedure TSQLRestStorageInMemory.LoadFromJSON(const aJSON: RawUTF8);
  40526. begin
  40527. LoadFromJSON(Pointer(aJSON),length(aJSON));
  40528. end;
  40529. procedure TSQLRestStorageInMemory.LoadFromJSON(JSONBuffer: PUTF8Char; JSONBufferLen: integer);
  40530. function IsSorted(U: PPUTF8Char; RowCount, FieldCount: integer): boolean;
  40531. var i: integer;
  40532. aID, lastID: TID;
  40533. begin
  40534. result := false;
  40535. lastID := 0;
  40536. for i := 1 to RowCount do begin
  40537. SetID(U^,aID);
  40538. if aID<=lastID then
  40539. exit else
  40540. lastID := aID;
  40541. inc(U,FieldCount);
  40542. end;
  40543. result := true;
  40544. end;
  40545. var T: TSQLTableJSON;
  40546. begin
  40547. fModified := false;
  40548. fValue.Clear;
  40549. if JSONBuffer=nil then
  40550. exit;
  40551. T := TSQLTableJSON.CreateFromTables([fStoredClass],'',JSONBuffer,JSONBufferLen);
  40552. try
  40553. if T.fFieldIndexID<0 then
  40554. exit; // no ID field -> load is impossible -> error
  40555. // ensure ID were stored in an increasing order
  40556. if not IsSorted(@T.fResults[T.fFieldIndexID+T.FieldCount],T.fRowCount,T.FieldCount) then begin
  40557. // force sorted by ID -> faster IDToIndex()
  40558. T.SortFields(T.fFieldIndexID,true,nil,sftInteger);
  40559. // if data is corrupted, IDs may not be unique -> check it now
  40560. if not IsSorted(@T.fResults[T.fFieldIndexID+T.FieldCount],T.fRowCount,T.FieldCount) then
  40561. exit; // some duplicated ID fields -> error
  40562. end;
  40563. // create TSQLRecord instances with data from T
  40564. T.ToObjectList(fValue,fStoredClass);
  40565. finally
  40566. T.Free;
  40567. end;
  40568. end;
  40569. procedure TSQLRestStorageInMemory.SaveToJSON(Stream: TStream; Expand: Boolean);
  40570. var i: integer;
  40571. W: TJSONSerializer;
  40572. begin
  40573. if self=nil then
  40574. exit;
  40575. StorageLock(false);
  40576. try
  40577. W := fStoredClassRecordProps.CreateJSONWriter(
  40578. Stream,Expand,true,ALL_FIELDS,fValue.Count);
  40579. try
  40580. if Expand then
  40581. W.Add('[');
  40582. for i := 0 to fValue.Count-1 do begin
  40583. if Expand then
  40584. W.AddCR; // for better readability
  40585. TSQLRecord(fValue.List[i]).GetJSONValues(W);
  40586. W.Add(',');
  40587. end;
  40588. W.EndJSONObject(fValue.Count,fValue.Count);
  40589. finally
  40590. W.Free;
  40591. end;
  40592. finally
  40593. StorageUnLock;
  40594. end;
  40595. end;
  40596. function TSQLRestStorageInMemory.SaveToJSON(Expand: Boolean): RawUTF8;
  40597. var MS: TRawByteStringStream;
  40598. begin
  40599. if self=nil then
  40600. result := '' else begin
  40601. MS := TRawByteStringStream.Create;
  40602. try
  40603. SaveToJSON(MS,Expand);
  40604. result := MS.DataString;
  40605. finally
  40606. MS.Free;
  40607. end;
  40608. end;
  40609. end;
  40610. function TSQLRestStorageInMemory.SaveToBinary: RawByteString;
  40611. var MS: TRawByteStringStream;
  40612. begin
  40613. if self=nil then
  40614. result := '' else begin
  40615. MS := TRawByteStringStream.Create;
  40616. try
  40617. SaveToBinary(MS);
  40618. result := MS.DataString;
  40619. finally
  40620. MS.Free;
  40621. end;
  40622. end;
  40623. end;
  40624. const
  40625. TSQLRESTSTORAGEINMEMORY_MAGIC = $A5ABA5A5;
  40626. function TSQLRestStorageInMemory.LoadFromBinary(Stream: TStream): boolean;
  40627. var R: TFileBufferReader;
  40628. MS: TMemoryStream;
  40629. i,n,f: integer;
  40630. ID32: TIntegerDynArray;
  40631. P: PAnsiChar;
  40632. aRecord: TSQLRecord;
  40633. lastID,newID: TID;
  40634. s: RawUTF8;
  40635. begin
  40636. result := false;
  40637. if self=nil then
  40638. exit;
  40639. MS := StreamUnSynLZ(Stream,TSQLRESTSTORAGEINMEMORY_MAGIC);
  40640. if MS<>nil then
  40641. with fStoredClassRecordProps do
  40642. try
  40643. // check header: expect same exact RTTI
  40644. R.OpenFrom(MS.Memory,MS.Size);
  40645. R.Read(s);
  40646. if (s<>'') and // new fixed format
  40647. not IdemPropNameU(s,'TSQLRecordProperties') then // old buggy format
  40648. exit;
  40649. if not CheckBinaryHeader(R) then
  40650. exit;
  40651. // read IDs
  40652. fModified := false;
  40653. fValue.Clear;
  40654. n := R.ReadVarUInt32Array(ID32);
  40655. fValue.Count := abs(n); // faster than fValue.Add() to allocate all at once
  40656. if n<0 then begin // was wkFakeMarker -> TID were stored as Int64
  40657. lastID := 0;
  40658. for i := 0 to -n-1 do begin
  40659. aRecord := fStoredClass.Create;
  40660. newID := lastID+R.ReadVarUInt64;
  40661. aRecord.fID := newID;
  40662. lastID := newID;
  40663. fValue.List[i] := aRecord;
  40664. end;
  40665. end else
  40666. for i := 0 to n-1 do begin
  40667. aRecord := fStoredClass.Create;
  40668. aRecord.fID := ID32[i];
  40669. fValue.List[i] := aRecord;
  40670. end;
  40671. // read content, grouped by field (for better compression)
  40672. P := R.CurrentMemory;
  40673. for f := 0 to Fields.Count-1 do
  40674. with Fields.List[f], fValue do
  40675. for i := 0 to Count-1 do begin
  40676. P := SetBinary(TSQLRecord(List[i]),P);
  40677. if P=nil then begin
  40678. fValue.Clear; // on error, reset whole
  40679. exit;
  40680. end;
  40681. end;
  40682. Result := true;
  40683. finally
  40684. R.Close;
  40685. MS.Free;
  40686. end;
  40687. end;
  40688. function TSQLRestStorageInMemory.LoadFromBinary(const Buffer: RawByteString): boolean;
  40689. var S: TStream;
  40690. begin
  40691. S := TRawByteStringStream.Create(Buffer);
  40692. try
  40693. result := LoadFromBinary(S);
  40694. finally
  40695. S.Free;
  40696. end;
  40697. end;
  40698. procedure TSQLRestStorageInMemory.LoadFromResource(ResourceName: string);
  40699. var S: TStream;
  40700. begin
  40701. if ResourceName = '' then
  40702. ResourceName := fStoredClass.ClassName;
  40703. S := TResourceStream.Create(HInstance,ResourceName,pointer(10));
  40704. try
  40705. if not LoadFromBinary(S) then
  40706. raise EORMException.CreateUTF8('%.LoadFromResource with invalid % content',
  40707. [self,fStoredClass]);
  40708. finally
  40709. S.Free;
  40710. end;
  40711. end;
  40712. function TSQLRestStorageInMemory.SaveToBinary(Stream: TStream): integer;
  40713. var W: TFileBufferWriter;
  40714. MS: THeapMemoryStream;
  40715. ID32: TIntegerDynArray;
  40716. i, f: integer;
  40717. hasInt64ID: boolean;
  40718. p: PID;
  40719. lastID,newID: TID;
  40720. begin
  40721. result := 0;
  40722. if (self=nil) or (Stream=nil) then
  40723. exit;
  40724. MS := THeapMemoryStream.Create;
  40725. W := TFileBufferWriter.Create(MS);
  40726. StorageLock(false);
  40727. try
  40728. with fStoredClassRecordProps do
  40729. try
  40730. // primitive magic and fields signature for file type identification
  40731. W.Write1(0); // ClassName='TSQLRecordProperties' in old buggy format
  40732. SaveBinaryHeader(W);
  40733. // write IDs
  40734. hasInt64ID := false;
  40735. SetLength(ID32,Count);
  40736. with fValue do
  40737. for i := 0 to Count-1 do begin
  40738. p := @TSQLRecord(List[i]).fID;
  40739. if p^>high(cardinal) then begin
  40740. hasInt64ID := true;
  40741. break;
  40742. end else
  40743. ID32[i] := PInteger(p)^;
  40744. end;
  40745. if hasInt64ID then begin
  40746. W.WriteVarUInt32(fValue.Count);
  40747. W.Write1(ord(wkFakeMarker)); // fake marker
  40748. lastID := 0;
  40749. with fValue do
  40750. for i := 0 to Count-1 do begin // a bit less efficient than wkSorted
  40751. newID := TSQLRecord(List[i]).fID;
  40752. if newID<=lastID then
  40753. raise EORMException.CreateUTF8('%.SaveToBinary(%): IDs not sorted',
  40754. [self,fStoredClass]);
  40755. W.WriteVarUInt64(newID-lastID);
  40756. lastID := newID;
  40757. end;
  40758. end else
  40759. W.WriteVarUInt32Array(ID32,Count,wkSorted); // efficient ID storage
  40760. // write content, grouped by field (for better compression)
  40761. for f := 0 to Fields.Count-1 do
  40762. with Fields.List[f], fValue do
  40763. for i := 0 to Count-1 do
  40764. GetBinary(TSQLRecord(List[i]),W);
  40765. W.Flush;
  40766. result := StreamSynLZ(MS,Stream,TSQLRESTSTORAGEINMEMORY_MAGIC);
  40767. finally
  40768. W.Free;
  40769. MS.Free;
  40770. end;
  40771. finally
  40772. StorageUnLock;
  40773. end;
  40774. end;
  40775. function TSQLRestStorageInMemory.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
  40776. var i: integer;
  40777. begin // TableModelIndex is not useful here
  40778. StorageLock(false);
  40779. try
  40780. i := IDToIndex(ID);
  40781. if i<0 then
  40782. result := '' else
  40783. result := TSQLRecord(fValue.List[i]).GetJSONValues(true,true,soSelect);
  40784. finally
  40785. StorageUnLock;
  40786. end;
  40787. end;
  40788. function TSQLRestStorageInMemory.GetOne(aID: TID): TSQLRecord;
  40789. var i: integer;
  40790. begin
  40791. StorageLock(false);
  40792. try
  40793. i := IDToIndex(aID);
  40794. if i<0 then
  40795. result := nil else begin
  40796. result := fStoredClass.Create;
  40797. CopyObject(TObject(fValue.List[i]),result);
  40798. result.fID := aID;
  40799. end;
  40800. finally
  40801. StorageUnLock;
  40802. end;
  40803. end;
  40804. function TSQLRestStorageInMemory.EngineUpdateFieldIncrement(TableModelIndex: integer;
  40805. ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
  40806. var i,err: integer;
  40807. P: TSQLPropInfo;
  40808. V: RawUTF8;
  40809. wasString: boolean;
  40810. int: Int64;
  40811. begin
  40812. result := false;
  40813. if (ID<0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
  40814. exit;
  40815. P := fStoredClassProps.Prop[FieldName];
  40816. if P=nil then
  40817. exit;
  40818. StorageLock(false);
  40819. try
  40820. i := IDToIndex(ID);
  40821. if i<0 then
  40822. exit;
  40823. P.GetValueVar(fValue.List[i],false,V,@wasstring);
  40824. int := GetInt64(pointer(V),err);
  40825. if wasString or (err<>0) then
  40826. exit;
  40827. Int64ToUtf8(int+Increment,V);
  40828. P.SetValueVar(fValue.List[i],V,false);
  40829. result := true;
  40830. finally
  40831. StorageUnLock;
  40832. end;
  40833. end;
  40834. function TSQLRestStorageInMemory.EngineUpdate(TableModelIndex: integer; ID: TID;
  40835. const SentData: RawUTF8): boolean;
  40836. var i: integer;
  40837. Orig,Rec: TSQLRecord;
  40838. begin
  40839. // this implementation will handle partial fields update (e.g.
  40840. // FillPrepare+BatchUpdate or TSQLRestServerRemoteDB.UpdateField)
  40841. // but TSQLRestStorageRecordBased.EngineUpdate won't
  40842. result := false;
  40843. if (ID<0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
  40844. exit;
  40845. if SentData='' then begin
  40846. result := True;
  40847. exit;
  40848. end;
  40849. StorageLock(true);
  40850. try
  40851. i := IDToIndex(ID);
  40852. if (i<0) or not RecordCanBeUpdated(fStoredClass,ID,seUpdate) then
  40853. exit;
  40854. if fUniqueFields<>nil then begin
  40855. Orig := TSQLRecord(fValue.List[i]);
  40856. Rec := Orig.CreateCopy; // copy since can be a partial update
  40857. Rec.FillFrom(SentData); // overwrite updated properties
  40858. if not UniqueFieldsUpdateOK(Rec,i) then begin
  40859. Rec.Free; // stored false property duplicated value -> error
  40860. exit;
  40861. end;
  40862. Orig.Free; // avoid memory leak
  40863. TSQLRecord(fValue.List[i]) := Rec; // update item in list
  40864. end else
  40865. // direct in-place partial update
  40866. TSQLRecord(fValue.List[i]).FillFrom(SentData);
  40867. fModified := true;
  40868. result := true;
  40869. if Owner<>nil then
  40870. Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,ID,SentData,nil);
  40871. finally
  40872. StorageUnLock;
  40873. end;
  40874. end;
  40875. function TSQLRestStorageInMemory.UpdateOne(Rec: TSQLRecord; const SentData: RawUTF8): boolean;
  40876. var i: integer;
  40877. begin
  40878. result := false;
  40879. if (Rec=nil) or (PSQLRecordClass(Rec)^<>fStoredClass) or (Rec.fID<=0) then
  40880. exit;
  40881. StorageLock(true);
  40882. try
  40883. i := IDToIndex(Rec.fID);
  40884. if (i<0) or not RecordCanBeUpdated(fStoredClass,Rec.fID,seUpdate) then
  40885. exit;
  40886. if (fUniqueFields<>nil) and not UniqueFieldsUpdateOK(Rec,i) then
  40887. exit; // stored false property duplicated value -> error
  40888. CopyObject(Rec,TObject(fValue.List[i]));
  40889. fModified := true;
  40890. result := true;
  40891. if Owner<>nil then
  40892. Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,Rec.fID,SentData,nil);
  40893. finally
  40894. StorageUnLock;
  40895. end;
  40896. end;
  40897. function TSQLRestStorageInMemory.UpdateOne(ID: TID;
  40898. const Values: TSQLVarDynArray): boolean;
  40899. var i: integer;
  40900. Orig,Rec: TSQLRecord;
  40901. begin
  40902. result := false;
  40903. if ID<=0 then
  40904. exit;
  40905. StorageLock(true);
  40906. try
  40907. i := IDToIndex(ID);
  40908. if (i<0) or not RecordCanBeUpdated(fStoredClass,ID,seUpdate) then
  40909. exit;
  40910. if fUniqueFields<>nil then begin
  40911. Orig := TSQLRecord(fValue.List[i]);
  40912. Rec := Orig.CreateCopy; // copy since can be a partial update
  40913. if (not Rec.SetFieldSQLVars(Values)) or
  40914. (not UniqueFieldsUpdateOK(Rec,i)) then begin
  40915. Rec.Free; // stored false property duplicated value -> error
  40916. exit;
  40917. end;
  40918. Orig.Free; // avoid memory leak
  40919. TSQLRecord(fValue.List[i]) := Rec;
  40920. end else
  40921. if not TSQLRecord(fValue.List[i]).SetFieldSQLVars(Values) then
  40922. exit;
  40923. fModified := true;
  40924. result := true;
  40925. if Owner<>nil then
  40926. Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,ID,
  40927. TSQLRecord(fValue.List[i]).GetJSONValues(True,False,soUpdate),nil);
  40928. finally
  40929. StorageUnLock;
  40930. end;
  40931. end;
  40932. function TSQLRestStorageInMemory.EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  40933. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  40934. var i: integer;
  40935. begin
  40936. result := false;
  40937. if (TableModelIndex<0) or (not BlobField^.IsBlob) or
  40938. (fModel.Tables[TableModelIndex]<>fStoredClass) then
  40939. exit;
  40940. StorageLock(false);
  40941. try
  40942. i := IDToIndex(aID);
  40943. if i<0 then
  40944. exit;
  40945. // get result blob directly from RTTI property description
  40946. BlobField.GetLongStrProp(fValue.List[i],RawByteString(BlobData));
  40947. result := true;
  40948. finally
  40949. StorageUnLock;
  40950. end;
  40951. end;
  40952. function TSQLRestStorageInMemory.RetrieveBlobFields(Value: TSQLRecord): boolean;
  40953. var i,f: integer;
  40954. begin
  40955. result := false;
  40956. if (Value<>nil) and (Value.fID>0) and (PSQLRecordClass(Value)^=fStoredClass) then
  40957. with Value.RecordProps do
  40958. if BlobFields<>nil then begin
  40959. StorageLock(false);
  40960. try
  40961. i := IDToIndex(Value.fID);
  40962. if i<0 then
  40963. exit;
  40964. for f := 0 to high(BlobFields) do
  40965. BlobFields[f].CopyValue(fValue.List[i],Value);
  40966. result := true;
  40967. finally
  40968. StorageUnLock;
  40969. end;
  40970. end;
  40971. end;
  40972. function TSQLRestStorageInMemory.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  40973. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  40974. var i: integer;
  40975. AffectedField: TSQLFieldBits;
  40976. begin
  40977. result := false;
  40978. if (aID<0) or (TableModelIndex<0) or (not BlobField^.IsBlob) or
  40979. (fModel.Tables[TableModelIndex]<>fStoredClass) then
  40980. exit;
  40981. StorageLock(true);
  40982. try
  40983. i := IDToIndex(aID);
  40984. if (i<0) or not RecordCanBeUpdated(fStoredClass,aID,seUpdate) then
  40985. exit;
  40986. // set blob value directly from RTTI property description
  40987. BlobField.SetLongStrProp(fValue.List[i],BlobData);
  40988. if Owner<>nil then begin
  40989. fStoredClassRecordProps.FieldBitsFromBlobField(BlobField,AffectedField);
  40990. Owner.InternalUpdateEvent(seUpdateBlob,fStoredClassProps.TableIndex,aID,'',@AffectedField);
  40991. end;
  40992. result := true;
  40993. finally
  40994. StorageUnLock;
  40995. end;
  40996. end;
  40997. function TSQLRestStorageInMemory.UpdateBlobFields(Value: TSQLRecord): boolean;
  40998. var i,f: integer;
  40999. begin
  41000. result := false;
  41001. if (Value<>nil) and (Value.fID>0) and (PSQLRecordClass(Value)^=fStoredClass) then
  41002. with Value.RecordProps do
  41003. if BlobFields<>nil then begin
  41004. StorageLock(true);
  41005. try
  41006. i := IDToIndex(Value.fID);
  41007. if (i<0) or not RecordCanBeUpdated(Table,Value.fID,seUpdate) then
  41008. exit;
  41009. for f := 0 to high(BlobFields) do
  41010. BlobFields[f].CopyValue(Value,fValue.List[i]);
  41011. if Owner<>nil then
  41012. Owner.InternalUpdateEvent(seUpdateBlob,fStoredClassProps.TableIndex,Value.fID,'',
  41013. @fStoredClassRecordProps.FieldBits[sftBlob]);
  41014. result := true;
  41015. finally
  41016. StorageUnLock;
  41017. end;
  41018. end else
  41019. result := true; // as TSQLRest.UpdateblobFields()
  41020. end;
  41021. function TSQLRestStorageInMemory.TableRowCount(Table: TSQLRecordClass): Int64;
  41022. begin
  41023. if Table<>fStoredClass then
  41024. result := 0 else
  41025. result := fValue.Count;
  41026. end;
  41027. function TSQLRestStorageInMemory.TableHasRows(Table: TSQLRecordClass): boolean;
  41028. begin
  41029. if Table<>fStoredClass then
  41030. result := false else
  41031. result := fValue.Count>0;
  41032. end;
  41033. function TSQLRestStorageInMemory.EngineUpdateField(TableModelIndex: integer;
  41034. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  41035. var SetField: TSQLPropInfo;
  41036. WhereValueString, SetValueString, SetValueJson: RawUTF8;
  41037. Where: TList;
  41038. i, ndx, WhereFieldIndex: integer;
  41039. SetValueWasString: boolean;
  41040. Rec: TSQLRecord;
  41041. begin
  41042. result := false;
  41043. if (TableModelIndex<0) or (fModel.Tables[TableModelIndex]<>fStoredClass) or
  41044. (SetFieldName='') or (SetValue='') or
  41045. (WhereFieldName='') or (WhereValue='') then
  41046. exit;
  41047. // handle destination field RTTI
  41048. SetField := fStoredClassRecordProps.Fields.ByRawUTF8Name(SetFieldName);
  41049. if SetField=nil then
  41050. exit; // don't allow setting ID field, which is Read Only
  41051. SetValueWasString := SetValue[1]='"';
  41052. if SetValueWasString then
  41053. UnQuoteSQLStringVar(pointer(SetValue),SetValueString) else
  41054. SetValueString := SetValue;
  41055. // handle search field RTTI
  41056. if IsRowID(pointer(WhereFieldName)) then begin
  41057. WhereFieldIndex := 0;
  41058. WhereValueString := WhereValue;
  41059. end else begin
  41060. WhereFieldIndex := fStoredClassRecordProps.Fields.IndexByName(WhereFieldName);
  41061. if WhereFieldIndex<0 then
  41062. exit;
  41063. inc(WhereFieldIndex); // FindWhereEqual() expects index = RTTI+1
  41064. end;
  41065. if WhereValue[1]='"' then
  41066. UnQuoteSQLStringVar(pointer(WhereValue),WhereValueString) else
  41067. WhereValueString := WhereValue;
  41068. // search indexes, then apply updates
  41069. Where := TList.Create;
  41070. StorageLock(true);
  41071. SetValueJson := ''; // alf: to circumvent FPC issues
  41072. try
  41073. // find matching Where[]
  41074. if FindWhereEqual(WhereFieldIndex,WhereValueString,AddIntegerDynArrayEvent,Where,0,0)=0 then
  41075. exit; // Where.Count=0 -> nothing to update
  41076. // check that all records can be updated
  41077. for i := 0 to Where.Count-1 do
  41078. if not RecordCanBeUpdated(fStoredClass,
  41079. TSQLRecord(fValue.List[PtrInt(Where.List[i])]).fID,seUpdate) then
  41080. exit; // one record update fails -> abort all
  41081. if fUniqueFields<>nil then
  41082. for i := 0 to fUniqueFields.Count-1 do
  41083. with TListFieldHash(fUniqueFields.List[i]) do
  41084. if Field=SetField then
  41085. if Where.Count>1 then // unique field can't allow multiple sets
  41086. exit else begin
  41087. SetField.SetValueVar(fSearchRec,SetValueString,false);
  41088. ndx := Find(fSearchRec);
  41089. if (ndx>=0) and (ndx<>PtrInt(Where.List[0])) then
  41090. exit; // duplicated entry error
  41091. end;
  41092. // update field value
  41093. for i := 0 to Where.Count-1 do begin
  41094. Rec := fValue.List[PtrInt(Where.List[i])];
  41095. SetField.SetValueVar(Rec,SetValueString,SetValueWasString);
  41096. fModified := true;
  41097. if Owner<>nil then begin
  41098. if SetValueJson='' then
  41099. JSONEncodeNameSQLValue(SetField.Name,SetValue,SetValueJson);
  41100. Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,Rec.fID,SetValueJson,nil);
  41101. end;
  41102. result := true;
  41103. end;
  41104. finally
  41105. StorageUnLock;
  41106. Where.Free;
  41107. end;
  41108. end;
  41109. procedure TSQLRestStorageInMemory.UpdateFile;
  41110. var F: TFileStream;
  41111. Timer: TPrecisionTimer;
  41112. begin
  41113. if (self=nil) or (not Modified) or (FileName='') then
  41114. exit;
  41115. Timer.Start;
  41116. StorageLock(false);
  41117. try
  41118. if fValue.Count=0 then
  41119. DeleteFile(FileName) else begin
  41120. F := TFileStream.Create(FileName,fmCreate);
  41121. try
  41122. if BinaryFile then
  41123. SaveToBinary(F) else
  41124. SaveToJSON(F,true);
  41125. F.Size := F.Position; // truncate file
  41126. finally
  41127. F.Free;
  41128. end;
  41129. end;
  41130. fModified := false;
  41131. finally
  41132. StorageUnLock;
  41133. end;
  41134. InternalLog('UpdateFile(%) done in %',
  41135. [fStoredClassRecordProps.SQLTableName,Timer.Stop],sllDB);
  41136. end;
  41137. procedure TSQLRestStorageInMemory.SetFileName(const aFileName: TFileName);
  41138. begin
  41139. if aFileName=fFileName then
  41140. exit;
  41141. fFileName := aFileName;
  41142. fModified := true;
  41143. end;
  41144. procedure TSQLRestStorageInMemory.SetBinaryFile(aBinary: boolean);
  41145. begin
  41146. if aBinary=fBinaryFile then
  41147. Exit;
  41148. fBinaryFile := aBinary;
  41149. fModified := true;
  41150. end;
  41151. procedure TSQLRestStorageInMemory.ReloadFromFile;
  41152. var JSON: RawUTF8;
  41153. Stream: TStream;
  41154. begin
  41155. if (fFileName<>'') and FileExists(fFileName) then begin
  41156. if fBinaryFile then begin
  41157. Stream := TSynMemoryStreamMapped.Create(fFileName);
  41158. try
  41159. LoadFromBinary(Stream)
  41160. finally
  41161. Stream.Free;
  41162. end;
  41163. end else begin
  41164. JSON := AnyTextFileToRawUTF8(fFileName,true);
  41165. LoadFromJSON(JSON);
  41166. end;
  41167. end;
  41168. end;
  41169. function TSQLRestStorageInMemory.SearchField(const FieldName, FieldValue: RawUTF8;
  41170. out ResultID: TIDDynArray): boolean;
  41171. var n, WhereField: integer;
  41172. {$ifndef CPU64}i: integer;{$endif}
  41173. Where: TList;
  41174. begin
  41175. result := false;
  41176. if (self=nil) or (fValue.Count=0) then
  41177. exit;
  41178. if IsRowID(pointer(FieldName)) then
  41179. WhereField := SYNTABLESTATEMENTWHEREID else begin
  41180. WhereField := fStoredClassRecordProps.Fields.IndexByName(FieldName);
  41181. if WhereField<0 then
  41182. exit;
  41183. inc(WhereField); // FindWhereEqual() expects index = RTTI+1
  41184. end;
  41185. Where := TList.Create;
  41186. try
  41187. StorageLock(false);
  41188. try
  41189. n := FindWhereEqual(WhereField,FieldValue,AddIntegerDynArrayEvent,Where,0,0);
  41190. finally
  41191. StorageUnLock;
  41192. end;
  41193. if n=0 then
  41194. exit;
  41195. SetLength(ResultID,n);
  41196. {$ifdef CPU64} // on x64 TList[]=Pointer does map an TID/Int64
  41197. MoveFast(Where.List[0],ResultID[0],n*sizeof(TID));
  41198. {$else}
  41199. with Where do
  41200. for i := 0 to Count-1 do
  41201. ResultID[i] := PPtrIntArray(List)^[i];
  41202. {$endif}
  41203. finally
  41204. Where.Free;
  41205. end;
  41206. end;
  41207. function TSQLRestStorageInMemory.SearchEvent(const FieldName, FieldValue: RawUTF8;
  41208. OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: integer): integer;
  41209. begin
  41210. result := 0;
  41211. if (self=nil) or (fValue.Count=0) or (FieldName='') then
  41212. exit;
  41213. StorageLock(false);
  41214. try
  41215. result := FindWhereEqual(FieldName,FieldValue,OnFind,Dest,FoundLimit,FoundOffset);
  41216. finally
  41217. StorageUnlock;
  41218. end;
  41219. end;
  41220. procedure TSQLRestStorageInMemory.DoCopyEvent(
  41221. aDest: pointer; aRec: TSQLRecord; aIndex: integer);
  41222. begin
  41223. if aDest<>nil then
  41224. PPointer(aDest)^ := aRec.CreateCopy;
  41225. end;
  41226. function TSQLRestStorageInMemory.SearchCopy(const FieldName, FieldValue: RawUTF8): pointer;
  41227. begin
  41228. if SearchEvent(FieldName,FieldValue,DoCopyEvent,@result,1,0)=0 then
  41229. result := nil;
  41230. end;
  41231. procedure TSQLRestStorageInMemory.DoInstanceEvent(aDest: pointer; aRec: TSQLRecord;
  41232. aIndex: integer);
  41233. begin
  41234. if aDest<>nil then
  41235. PPointer(aDest)^ := aRec;
  41236. end;
  41237. function TSQLRestStorageInMemory.SearchInstance(const FieldName, FieldValue: RawUTF8): pointer;
  41238. begin
  41239. if SearchEvent(FieldName,FieldValue,DoInstanceEvent,@result,1,0)=0 then
  41240. result := nil;
  41241. end;
  41242. procedure TSQLRestStorageInMemory.DoIndexEvent(aDest: pointer; aRec: TSQLRecord;
  41243. aIndex: integer);
  41244. begin
  41245. if aDest<>nil then
  41246. PInteger(aDest)^ := aIndex;
  41247. end;
  41248. function TSQLRestStorageInMemory.SearchIndex(const FieldName, FieldValue: RawUTF8): integer;
  41249. begin
  41250. if SearchEvent(FieldName,FieldValue,DoIndexEvent,@result,1,0)=0 then
  41251. result := -1;
  41252. end;
  41253. function TSQLRestStorageInMemory.SearchCount(const FieldName, FieldValue: RawUTF8): integer;
  41254. begin
  41255. result := SearchEvent(FieldName,FieldValue,DoNothingEvent,nil,0,0);
  41256. end;
  41257. { TSQLRestStorageInMemoryExternal }
  41258. constructor TSQLRestStorageInMemoryExternal.Create(aClass: TSQLRecordClass;
  41259. aServer: TSQLRestServer; const aFileName: TFileName = ''; aBinaryFile: boolean=false);
  41260. begin
  41261. inherited Create(aClass,aServer,aFileName,aBinaryFile);
  41262. fStorageLockShouldIncreaseOwnerInternalState := false; // done by overriden StorageLock()
  41263. end;
  41264. procedure TSQLRestStorageInMemoryExternal.StorageLock(WillModifyContent: boolean);
  41265. begin
  41266. inherited StorageLock(WillModifyContent);
  41267. if WillModifyContent and (Owner<>nil) then
  41268. Owner.FlushInternalDBCache;
  41269. end;
  41270. { TSQLRestStorageRemote }
  41271. constructor TSQLRestStorageRemote.Create(aClass: TSQLRecordClass;
  41272. aServer: TSQLRestServer; aRemoteRest: TSQLRest);
  41273. begin
  41274. if aRemoteRest=nil then
  41275. raise EORMException.CreateUTF8('%.Create(nil)',[self]);
  41276. inherited Create(aClass,aServer);
  41277. fRemoteTableIndex := aRemoteRest.Model.GetTableIndexExisting(aClass);
  41278. fRemoteRest := aRemoteRest;
  41279. end;
  41280. function TSQLRestStorageRemote.EngineAdd(TableModelIndex: integer;
  41281. const SentData: RawUTF8): TID;
  41282. begin
  41283. result := fRemoteRest.EngineAdd(fRemoteTableIndex,SentData);
  41284. end;
  41285. function TSQLRestStorageRemote.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
  41286. begin
  41287. result := fRemoteRest.EngineDelete(fRemoteTableIndex,ID);
  41288. end;
  41289. function TSQLRestStorageRemote.EngineDeleteWhere(TableModelIndex: Integer;
  41290. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  41291. begin
  41292. result := fRemoteRest.EngineDeleteWhere(fRemoteTableIndex,SQLWhere,IDs);
  41293. end;
  41294. function TSQLRestStorageRemote.EngineExecute(const aSQL: RawUTF8): boolean;
  41295. begin
  41296. result := fRemoteRest.EngineExecute(aSQL);
  41297. end;
  41298. function TSQLRestStorageRemote.EngineList(const SQL: RawUTF8;
  41299. ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
  41300. begin
  41301. result := fRemoteRest.EngineList(SQL,ForceAJAX,ReturnedRowCount);
  41302. end;
  41303. function TSQLRestStorageRemote.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
  41304. begin
  41305. result := fRemoteRest.EngineRetrieve(fRemoteTableIndex,ID);
  41306. end;
  41307. function TSQLRestStorageRemote.EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  41308. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  41309. begin
  41310. if (self=nil) or (BlobField=nil) then
  41311. result := false else
  41312. result := fRemoteRest.EngineRetrieveBlob(fRemoteTableIndex,aID,BlobField,BlobData);
  41313. end;
  41314. function TSQLRestStorageRemote.EngineUpdate(TableModelIndex: integer;
  41315. ID: TID; const SentData: RawUTF8): boolean;
  41316. begin
  41317. result := fRemoteRest.EngineUpdate(fRemoteTableIndex,ID,SentData);
  41318. end;
  41319. function TSQLRestStorageRemote.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  41320. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  41321. begin
  41322. if (self=nil) or (BlobField=nil) then
  41323. result := false else
  41324. result := fRemoteRest.EngineUpdateBlob(fRemoteTableIndex,aID,BlobField,BlobData);
  41325. end;
  41326. function TSQLRestStorageRemote.EngineUpdateField(TableModelIndex: integer;
  41327. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  41328. begin
  41329. result := fRemoteRest.EngineUpdateField(fRemoteTableIndex,SetFieldName,SetValue,WhereFieldName,WhereValue);
  41330. end;
  41331. function TSQLRestStorageRemote.EngineUpdateFieldIncrement(TableModelIndex: integer;
  41332. ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
  41333. begin
  41334. result := fRemoteRest.EngineUpdateFieldIncrement(fRemoteTableIndex,ID,FieldName,Increment);
  41335. end;
  41336. { TSQLRestStorageShard }
  41337. const MIN_SHARD = 1000;
  41338. constructor TSQLRestStorageShard.Create(aClass: TSQLRecordClass;
  41339. aServer: TSQLRestServer; aShardRange: TID; aOptions: TSQLRestStorageShardOptions);
  41340. var i,n: integer;
  41341. begin
  41342. if aShardRange<MIN_SHARD then
  41343. raise EORMException.CreateUTF8('%.Create(%,aShardRange=%<%) does not make sense',
  41344. [self,aClass,aShardRange,MIN_SHARD]);
  41345. inherited Create(aClass,aServer);
  41346. fShardRange := aShardRange;
  41347. fShardLast := cardinal(-1);
  41348. fOptions := aOptions;
  41349. InitShards; // set fShards[], fShardLast and fShardLastID
  41350. n := length(fShards);
  41351. fShardNextID := n*fShardRange+1;
  41352. SetLength(fShardTableIndex,n);
  41353. for i := 0 to fShardLast do
  41354. if fShards[i]=nil then
  41355. fShardTableIndex[i] := -1 else
  41356. fShardTableIndex[i] := fShards[i].Model.GetTableIndexExisting(aClass);
  41357. end;
  41358. destructor TSQLRestStorageShard.Destroy;
  41359. var i,j: integer;
  41360. rest: TSQLRest;
  41361. begin
  41362. try
  41363. if not (ssoNoConsolidateAtDestroy in fOptions) then
  41364. ConsolidateShards;
  41365. finally
  41366. inherited Destroy;
  41367. for i := 0 to high(fShards) do begin
  41368. rest := fShards[i];
  41369. if rest=nil then
  41370. continue;
  41371. rest.Free;
  41372. for j := i+1 to high(fShards) do
  41373. if fShards[j]=rest then
  41374. fShards[j] := nil; // same instance re-used in fShards[]
  41375. end;
  41376. end;
  41377. end;
  41378. procedure TSQLRestStorageShard.ConsolidateShards;
  41379. begin // do nothing by default
  41380. end;
  41381. procedure TSQLRestStorageShard.RemoveShard(aShardIndex: integer);
  41382. begin
  41383. StorageLock(true);
  41384. try
  41385. if (fShards<>nil) and (cardinal(aShardIndex)<=fShardLast) then
  41386. FreeAndNil(fShards[aShardIndex]);
  41387. finally
  41388. StorageUnLock;
  41389. end;
  41390. end;
  41391. procedure TSQLRestStorageShard.InternalAddNewShard;
  41392. var rest: TSQLRest;
  41393. begin
  41394. {$ifdef WITHLOG}
  41395. fLogClass.Enter('%.InternalAddNewShard: #% for %',[fShardLast+1,fStoredClass],self);
  41396. {$endif}
  41397. rest := InitNewShard;
  41398. if rest=nil then
  41399. raise EORMException.CreateUTF8('%.InitNewShard(%) =nil',[self,fStoredClass]);
  41400. inc(fShardNextID,fShardRange);
  41401. SetLength(fShardTableIndex,fShardLast+1);
  41402. fShardTableIndex[fShardLast] := rest.Model.GetTableIndexExisting(fStoredClass);
  41403. end;
  41404. function TSQLRestStorageShard.ShardFromID(aID: TID; out aShardTableIndex: integer;
  41405. out aShard: TSQLRest; aOccasion: TSQLOccasion; aShardIndex: PInteger): boolean;
  41406. var ndx: cardinal;
  41407. begin
  41408. result := false;
  41409. if aID<=0 then
  41410. exit;
  41411. case aOccasion of
  41412. soUpdate:
  41413. if ssoNoUpdate in fOptions then
  41414. exit;
  41415. soDelete:
  41416. if ssoNoDelete in fOptions then
  41417. exit;
  41418. end;
  41419. EnterCriticalSection(fStorageCriticalSection);
  41420. try
  41421. ndx := (aID-1) div fShardRange;
  41422. if (ndx<=fShardLast) and (fShards[ndx]<>nil) then begin
  41423. case aOccasion of
  41424. soUpdate:
  41425. if (ssoNoUpdateButLastShard in fOptions) and (ndx<>fShardLast) then
  41426. exit;
  41427. soDelete:
  41428. if (ssoNoDeleteButLastShard in fOptions) and (ndx<>fShardLast) then
  41429. exit;
  41430. end;
  41431. aShard := fShards[ndx];
  41432. aShardTableIndex := fShardTableIndex[ndx];
  41433. if aShardIndex<>nil then
  41434. aShardIndex^ := ndx;
  41435. result := true;
  41436. end;
  41437. finally
  41438. LeaveCriticalSection(fStorageCriticalSection);
  41439. end;
  41440. end;
  41441. function TSQLRestStorageShard.EngineAdd(TableModelIndex: integer;
  41442. const SentData: RawUTF8): TID;
  41443. var data: RawUTF8;
  41444. i: Integer;
  41445. begin
  41446. if JSONGetID(pointer(SentData),result) then
  41447. raise EORMException.CreateUTF8('%.EngineAdd(%) unexpected ID in %',
  41448. [self,fStoredClass,SentData]);
  41449. StorageLock(true);
  41450. try
  41451. inc(fShardLastID);
  41452. if fShardLastID>=fShardNextID then begin
  41453. InternalAddNewShard;
  41454. if fShardLastID>=fShardNextID then
  41455. raise EORMException.CreateUTF8('%.EngineAdd(%) fShardNextID',[self,fStoredClass]);
  41456. end;
  41457. result := fShardLastID;
  41458. i := PosEx('{',SentData);
  41459. if i=0 then
  41460. data := FormatUTF8('{ID:%}',[result]) else begin
  41461. data := SentData;
  41462. insert(FormatUTF8('ID:%,',[result]),data,i+1);
  41463. end;
  41464. if fShardBatch<>nil then
  41465. InternalShardBatch(fShardLast).RawAdd(data) else begin
  41466. if fShards[fShardLast].EngineAdd(fShardTableIndex[fShardLast],data)<>result then begin
  41467. InternalLog('EngineAdd(%) error adding ID=%',[fStoredClass,result],sllError);
  41468. result := 0;
  41469. end;
  41470. end;
  41471. finally
  41472. StorageUnLock;
  41473. end;
  41474. end;
  41475. function TSQLRestStorageShard.EngineDelete(TableModelIndex: integer;
  41476. ID: TID): boolean;
  41477. var tableIndex,shardIndex: integer;
  41478. rest: TSQLRest;
  41479. begin
  41480. StorageLock(true);
  41481. try
  41482. if not ShardFromID(ID,tableIndex,rest,soDelete,@shardIndex) then
  41483. result := false else
  41484. if fShardBatch<>nil then
  41485. result := InternalShardBatch(shardIndex).Delete(ID)>=0 else
  41486. result := rest.EngineDelete(tableIndex,ID);
  41487. finally
  41488. StorageUnLock;
  41489. end;
  41490. end;
  41491. function TSQLRestStorageShard.EngineDeleteWhere(TableModelIndex: integer;
  41492. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  41493. var i: integer;
  41494. ndx: cardinal;
  41495. id: array of TInt64DynArray; // IDs split per shard
  41496. sql: RawUTF8;
  41497. begin
  41498. result := false;
  41499. if (IDs=nil) or (ssoNoDelete in fOptions) then
  41500. exit;
  41501. StorageLock(true);
  41502. try
  41503. SetLength(id,fShardLast+1);
  41504. for i := 0 to high(IDs) do begin
  41505. ndx := (IDs[i]-1) div fShardRange;
  41506. if (ndx>=fShardLast) or (fShards[ndx]=nil) then
  41507. continue;
  41508. if (ssoNoDeleteButLastShard in fOptions) and (ndx<>fShardLast) then
  41509. continue;
  41510. AddInt64(id[ndx],IDs[i]);
  41511. end;
  41512. result := true;
  41513. for i := 0 to high(id) do
  41514. if id[i]<>nil then begin
  41515. sql := Int64DynArrayToCSV(id[i],length(id[i]),'ID in (',')');
  41516. if not fShards[i].EngineDeleteWhere(fShardTableIndex[i],sql,TIDDynArray(id[i])) then
  41517. result := false;
  41518. end;
  41519. finally
  41520. StorageUnLock;
  41521. end;
  41522. end;
  41523. function TSQLRestStorageShard.EngineExecute(const aSQL: RawUTF8): boolean;
  41524. begin
  41525. StorageLock(false);
  41526. try
  41527. if (integer(fShardLast)>=0) and not (ssoNoExecute in fOptions) then
  41528. result := fShards[fShardLast].EngineExecute(aSQL) else
  41529. result := false;
  41530. finally
  41531. StorageUnLock;
  41532. end;
  41533. end;
  41534. function TSQLRestStorageShard.TableHasRows(Table: TSQLRecordClass): boolean;
  41535. begin
  41536. result := fShards<>nil;
  41537. end;
  41538. function TSQLRestStorageShard.TableRowCount(Table: TSQLRecordClass): Int64;
  41539. var i: integer;
  41540. begin
  41541. result := 0;
  41542. InternalLog('TableRowCount(%) may take a while',[fStoredClass],sllWarning);
  41543. for i := 0 to high(fShards) do
  41544. if fShards[i]<>nil then
  41545. inc(result,fShards[i].TableRowCount(fStoredClass));
  41546. end;
  41547. function TSQLRestStorageShard.EngineList(const SQL: RawUTF8;
  41548. ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
  41549. var ResCount: PtrInt;
  41550. begin
  41551. result := ''; // indicates error occurred
  41552. StorageLock(false);
  41553. try
  41554. ResCount := 0;
  41555. if IdemPropNameU(fBasicSQLCount,SQL) then begin
  41556. FormatUTF8('[{"Count(*)":%}]'#$A,[TableRowCount(fStoredClass)],result);
  41557. ResCount := 1;
  41558. end else
  41559. if IdemPropNameU(fBasicSQLHasRows[false],SQL) or
  41560. IdemPropNameU(fBasicSQLHasRows[true],SQL) then
  41561. if fShards<>nil then begin // return one row with fake ID=1
  41562. result := '[{"RowID":1}]'#$A;
  41563. ResCount := 1;
  41564. end else
  41565. result := '{"fieldCount":1,"values":["RowID"]}'#$A else begin
  41566. if (integer(fShardLast)>=0) and not (ssoNoList in fOptions) then
  41567. result := fShards[fShardLast].EngineList(SQL,ForceAJAX,@ResCount);
  41568. end;
  41569. if ReturnedRowCount<>nil then
  41570. ReturnedRowCount^ := ResCount;
  41571. finally
  41572. StorageUnLock;
  41573. end;
  41574. end;
  41575. function TSQLRestStorageShard.EngineRetrieve(TableModelIndex: integer;
  41576. ID: TID): RawUTF8;
  41577. var tableIndex: integer;
  41578. rest: TSQLRest;
  41579. begin
  41580. StorageLock(false);
  41581. try
  41582. if not ShardFromID(ID,tableIndex,rest) then
  41583. result := '' else
  41584. result := rest.EngineRetrieve(tableIndex,ID);
  41585. finally
  41586. StorageUnLock;
  41587. end;
  41588. end;
  41589. function TSQLRestStorageShard.EngineRetrieveBlob(TableModelIndex: integer;
  41590. aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  41591. var tableIndex: integer;
  41592. rest: TSQLRest;
  41593. begin
  41594. StorageLock(false);
  41595. try
  41596. if not ShardFromID(aID,tableIndex,rest) then
  41597. result := false else
  41598. result := rest.EngineRetrieveBlob(tableIndex,aID,BlobField,BlobData);
  41599. finally
  41600. StorageUnLock;
  41601. end;
  41602. end;
  41603. function TSQLRestStorageShard.EngineUpdate(TableModelIndex: integer;
  41604. ID: TID; const SentData: RawUTF8): boolean;
  41605. var tableIndex,shardIndex: integer;
  41606. rest: TSQLRest;
  41607. begin
  41608. StorageLock(true);
  41609. try
  41610. if not ShardFromID(ID,tableIndex,rest,soUpdate,@shardIndex) then
  41611. result := false else
  41612. if fShardBatch<>nil then begin
  41613. InternalShardBatch(shardIndex).RawUpdate(SentData,ID);
  41614. result := true;
  41615. end else
  41616. result := rest.EngineUpdate(tableIndex,ID,SentData);
  41617. finally
  41618. StorageUnLock;
  41619. end;
  41620. end;
  41621. function TSQLRestStorageShard.EngineUpdateBlob(TableModelIndex: integer;
  41622. aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  41623. var tableIndex: integer;
  41624. rest: TSQLRest;
  41625. begin
  41626. result := false;
  41627. StorageLock(true);
  41628. try
  41629. if ShardFromID(aID,tableIndex,rest,soUpdate) then
  41630. result := rest.EngineUpdateBlob(tableIndex,aID,BlobField,BlobData);
  41631. finally
  41632. StorageUnLock;
  41633. end;
  41634. end;
  41635. function TSQLRestStorageShard.EngineUpdateField(TableModelIndex: integer;
  41636. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  41637. begin
  41638. result := false;
  41639. StorageLock(true);
  41640. try
  41641. if not ((ssoNoUpdate in fOptions) or (ssoNoUpdateField in fOptions)) then
  41642. result := fShards[fShardLast].EngineUpdateField(fShardTableIndex[fShardLast],
  41643. SetFieldName,SetValue,WhereFieldName,WhereValue);
  41644. finally
  41645. StorageUnLock;
  41646. end;
  41647. end;
  41648. function TSQLRestStorageShard.EngineUpdateFieldIncrement(
  41649. TableModelIndex: integer; ID: TID; const FieldName: RawUTF8;
  41650. Increment: Int64): boolean;
  41651. var tableIndex: integer;
  41652. rest: TSQLRest;
  41653. begin
  41654. result := false;
  41655. StorageLock(true);
  41656. try
  41657. if ShardFromID(ID,tableIndex,rest,soUpdate) then
  41658. result := rest.EngineUpdateFieldIncrement(tableIndex,ID,FieldName,Increment);
  41659. finally
  41660. StorageUnLock;
  41661. end;
  41662. end;
  41663. function TSQLRestStorageShard.InternalBatchStart(Method: TSQLURIMethod;
  41664. BatchOptions: TSQLRestBatchOptions): boolean;
  41665. begin
  41666. result := false;
  41667. if ssoNoBatch in fOptions then
  41668. exit;
  41669. StorageLock(true); // protected by try..finally in TSQLRestServer.RunBatch
  41670. try
  41671. if fShardBatch<>nil then
  41672. raise EORMException.CreateUTF8('%.InternalBatchStop should have been called',[self]);
  41673. SetLength(fShardBatch,fShardLast+1);
  41674. result := true;
  41675. finally
  41676. if not result then // release lock on error
  41677. StorageUnLock;
  41678. end;
  41679. end;
  41680. function TSQLRestStorageShard.InternalShardBatch(ShardIndex: integer): TSQLRestBatch;
  41681. begin
  41682. if cardinal(ShardIndex)>fShardLast then
  41683. raise EORMException.CreateUTF8('%.InternalShardBatch(%)',[self,ShardIndex]);
  41684. if fShardBatch=nil then
  41685. raise EORMException.CreateUTF8('%.InternalBatchStart should have been called',[self]);
  41686. if ShardIndex>=length(fShardBatch) then
  41687. SetLength(fShardBatch,ShardIndex+1); // InitNewShard just called
  41688. if fShardBatch[ShardIndex]=nil then
  41689. if fShards[ShardIndex]<>nil then
  41690. fShardBatch[ShardIndex] := TSQLRestBatch.Create(
  41691. fShards[ShardIndex],fStoredClass,10000,[boExtendedJSON]) else
  41692. raise EORMException.CreateUTF8('%.InternalShardBatch fShards[%]=nil',[self,ShardIndex]);
  41693. result := fShardBatch[ShardIndex];
  41694. end;
  41695. procedure TSQLRestStorageShard.InternalBatchStop;
  41696. var i: integer;
  41697. begin
  41698. try
  41699. for i := 0 to high(fShardBatch) do
  41700. if fShardBatch[i]<>nil then
  41701. if fShards[i].BatchSend(fShardBatch[i])<>HTML_SUCCESS then
  41702. InternalLog('%.InternalBatchStop(%): %.BatchSend failed for shard #%',
  41703. [ClassType,fStoredClass,fShards[i].ClassType,i],sllWarning);
  41704. finally
  41705. ObjArrayClear(fShardBatch);
  41706. StorageUnLock;
  41707. end;
  41708. end;
  41709. { TListFieldHash }
  41710. function TListFieldHash.Compare(Item1,Item2: TObject): boolean;
  41711. begin
  41712. result := fProp.CompareValue(Item1,Item2,CaseInsensitive)=0;
  41713. end;
  41714. function TListFieldHash.Count: integer;
  41715. begin
  41716. result := fValues.Count;
  41717. end;
  41718. constructor TListFieldHash.Create(aValues: TList; aField: TSQLPropInfo;
  41719. aCaseInsensitive: boolean);
  41720. begin
  41721. fValues := aValues;
  41722. fField := aField.PropertyIndex;
  41723. fProp := aField;
  41724. fCaseInsensitive := aCaseInsensitive;
  41725. end;
  41726. function TListFieldHash.Hash(Item: TObject): cardinal;
  41727. begin
  41728. result := fProp.GetHash(Item,CaseInsensitive);
  41729. if result=0 then
  41730. result := 1; // HASH=0 is used to indicate a void slot in fHash[]
  41731. end;
  41732. function TListFieldHash.Get(Index: integer): TObject;
  41733. begin
  41734. with fValues do
  41735. if cardinal(Index)<cardinal(Count) then
  41736. result := List[Index] else
  41737. result := nil;
  41738. end;
  41739. function TListFieldHash.Scan(Item: TObject; ListCount: integer): integer;
  41740. begin
  41741. for result := 0 to ListCount-1 do
  41742. if fProp.CompareValue(fValues.List[result],Item,CaseInsensitive)=0 then
  41743. exit;
  41744. result := -1;
  41745. end;
  41746. { TSQLRestStorage }
  41747. constructor TSQLRestStorage.Create(aClass: TSQLRecordClass;
  41748. aServer: TSQLRestServer);
  41749. begin
  41750. inherited Create(nil);
  41751. if aClass=nil then
  41752. raise EBusinessLayerException.CreateUTF8('%.Create(aClass=nil)',[self]);
  41753. InitializeCriticalSection(fStorageCriticalSection);
  41754. fStoredClass := aClass;
  41755. fStoredClassRecordProps := aClass.RecordProps;
  41756. if aServer<>nil then begin
  41757. fOwner := aServer;
  41758. fModel := aServer.Model;
  41759. fStoredClassProps := fModel.Props[aClass];
  41760. end else
  41761. // if no server is defined, simply use the first model using this class
  41762. if fStoredClassRecordProps.fModel<>nil then
  41763. with fStoredClassRecordProps.fModel[0] do begin
  41764. fModel := Model;
  41765. fStoredClassProps := Properties;
  41766. end;
  41767. fIsUnique := fStoredClassRecordProps.IsUniqueFieldsBits;
  41768. fBasicSQLCount := 'SELECT COUNT(*) FROM '+fStoredClassRecordProps.SQLTableName;
  41769. fBasicSQLHasRows[false] := 'SELECT RowID FROM '+fStoredClassRecordProps.SQLTableName+' LIMIT 1';
  41770. fBasicSQLHasRows[true] := fBasicSQLHasRows[false];
  41771. system.delete(fBasicSQLHasRows[true],8,3);
  41772. end;
  41773. destructor TSQLRestStorage.Destroy;
  41774. begin
  41775. inherited;
  41776. if fStorageCriticalSectionCount<>0 then
  41777. raise EORMException.CreateUTF8('%.Destroy with CS=%',[self,fStorageCriticalSectionCount]);
  41778. DeleteCriticalSection(fStorageCriticalSection);
  41779. end;
  41780. procedure TSQLRestStorage.BeginCurrentThread(Sender: TThread);
  41781. begin // called by TSQLRestServer.BeginCurrentThread
  41782. // nothing to do in this basic REST static class
  41783. end;
  41784. procedure TSQLRestStorage.EndCurrentThread(Sender: TThread);
  41785. begin // called by TSQLRestServer.EndCurrentThread
  41786. // nothing to do in this basic REST static class
  41787. end;
  41788. function TSQLRestStorage.ServiceContainer: TServiceContainer;
  41789. begin
  41790. result := nil;
  41791. end;
  41792. function TSQLRestStorage.CreateSQLMultiIndex(Table: TSQLRecordClass;
  41793. const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8): boolean;
  41794. begin
  41795. result := false; // not implemented in this basic REST static class
  41796. end;
  41797. function TSQLRestStorage.SearchField(const FieldName: RawUTF8;
  41798. FieldValue: Int64; out ResultID: TIDDynArray): boolean;
  41799. begin
  41800. result := SearchField(FieldName,Int64ToUTF8(FieldValue),ResultID);
  41801. end;
  41802. function TSQLRestStorage.RecordCanBeUpdated(Table: TSQLRecordClass;
  41803. ID: TID; Action: TSQLEvent; ErrorMsg: PRawUTF8 = nil): boolean;
  41804. begin
  41805. result := ((Owner=nil) or Owner.RecordCanBeUpdated(Table,ID,Action,ErrorMsg));
  41806. end;
  41807. function TSQLRestStorage.RefreshedAndModified: boolean;
  41808. begin
  41809. result := false; // no refresh necessary with "normal" static tables
  41810. end;
  41811. procedure TSQLRestStorage.StorageLock(WillModifyContent: boolean);
  41812. begin
  41813. if fStorageLockLogTrace then
  41814. InternalLog('StorageLock % %',[fStoredClass,fStorageCriticalSectionCount],sllTrace);
  41815. EnterCriticalSection(fStorageCriticalSection);
  41816. inc(fStorageCriticalSectionCount);
  41817. if WillModifyContent and
  41818. fStorageLockShouldIncreaseOwnerInternalState and (Owner<>nil) then
  41819. inc(Owner.InternalState);
  41820. end;
  41821. procedure TSQLRestStorage.StorageUnLock;
  41822. begin
  41823. dec(fStorageCriticalSectionCount);
  41824. if fStorageLockLogTrace then
  41825. InternalLog('StorageUnlock % %',[fStoredClass,fStorageCriticalSectionCount],sllTrace);
  41826. if fStorageCriticalSectionCount<0 then
  41827. raise EORMException.CreateUTF8('%.StorageUnLock with CS=%',
  41828. [self,fStorageCriticalSectionCount]);
  41829. LeaveCriticalSection(fStorageCriticalSection);
  41830. end;
  41831. function TSQLRestStorage.GetCurrentSessionUserID: TID;
  41832. begin
  41833. if fOwner=nil then
  41834. result := 0 else
  41835. result := fOwner.GetCurrentSessionUserID;
  41836. end;
  41837. procedure TSQLRestStorage.RecordVersionFieldHandle(Occasion: TSQLOccasion;
  41838. var Decoder: TJSONObjectDecoder);
  41839. begin
  41840. if fStoredClassRecordProps.RecordVersionField=nil then
  41841. exit;
  41842. if Owner=nil then
  41843. raise EORMException.CreateUTF8('Owner=nil for %.%: TRecordVersion',
  41844. [fStoredClass,fStoredClassRecordProps.RecordVersionField.Name]);
  41845. Owner.InternalRecordVersionHandle(Occasion,fStoredClassProps.TableIndex,
  41846. Decoder,fStoredClassRecordProps.RecordVersionField);
  41847. end;
  41848. function TSQLRestStorage.UnLock(Table: TSQLRecordClass; aID: TID): boolean;
  41849. begin
  41850. result := Model.UnLock(Table,aID);
  41851. end;
  41852. function TSQLRestStorage.AdaptSQLForEngineList(var SQL: RawUTF8): boolean;
  41853. begin
  41854. if fStoredClassProps=nil then
  41855. result := false else begin
  41856. result := IdemPropNameU(fStoredClassProps.SQL.SelectAllWithRowID,SQL);
  41857. if result then
  41858. SQL := fStoredClassProps.SQL.SelectAllWithID else
  41859. result := IdemPropNameU(fStoredClassProps.SQL.SelectAllWithID,SQL);
  41860. end;
  41861. end;
  41862. function TSQLRestStorage.GetStoredClassName: RawUTF8;
  41863. begin
  41864. if self=nil then
  41865. result := '' else
  41866. ShortStringToAnsi7String(PShortString(PPointer(PtrInt(fStoredClass)+vmtClassName)^)^,result);
  41867. end;
  41868. { TSQLRestServerFullMemory }
  41869. constructor TSQLRestServerFullMemory.Create(aModel: TSQLModel;
  41870. aHandleUserAuthentication: boolean);
  41871. var t: integer;
  41872. begin
  41873. inherited Create(aModel,aHandleUserAuthentication);
  41874. fStaticDataCount := length(fModel.Tables);
  41875. SetLength(fStorage,fStaticDataCount);
  41876. for t := 0 to fStaticDataCount-1 do begin
  41877. fStorage[t] := (StaticDataCreate(fModel.Tables[t]) as TSQLRestStorageInMemory);
  41878. fStorage[t].fStorageLockShouldIncreaseOwnerInternalState := true;
  41879. end;
  41880. end;
  41881. constructor TSQLRestServerFullMemory.Create(aModel: TSQLModel;
  41882. const aFileName: TFileName; aBinaryFile, aHandleUserAuthentication: boolean);
  41883. begin
  41884. fFileName := aFileName;
  41885. fBinaryFile := aBinaryFile;
  41886. Create(aModel,aHandleUserAuthentication);
  41887. LoadFromFile;
  41888. CreateMissingTables(0,[]);
  41889. end;
  41890. constructor TSQLRestServerFullMemory.CreateWithOwnedAuthenticatedModel(
  41891. const Tables: array of TSQLRecordClass; const aUserName,aHashedPassword: RawUTF8;
  41892. aRoot: RawUTF8);
  41893. var User: TSQLAuthUser;
  41894. begin
  41895. if aRoot='' then
  41896. aRoot := 'root';
  41897. if aUserName='' then
  41898. CreateWithOwnModel(Tables,false,aRoot) else begin
  41899. CreateWithOwnModel(Tables,true,aRoot);
  41900. CreateMissingTables(0,[itoNoAutoCreateUsers]);
  41901. User := TSQLAuthUser.Create;
  41902. try
  41903. User.LogonName := aUserName;
  41904. User.PasswordHashHexa := aHashedPassword;
  41905. User.GroupRights := TSQLAuthGroup(2); // member of 'Supervisor' group
  41906. Add(User,true);
  41907. finally
  41908. User.Free;
  41909. end;
  41910. end;
  41911. end;
  41912. constructor TSQLRestServerFullMemory.RegisteredClassCreateFrom(aModel: TSQLModel;
  41913. aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition);
  41914. begin
  41915. fFileName := UTF8ToString(aDefinition.ServerName);
  41916. fBinaryFile := aDefinition.DatabaseName<>''; // DefinitionTo() set 'binary'
  41917. Create(aModel,aServerHandleAuthentication);
  41918. LoadFromFile;
  41919. end;
  41920. procedure TSQLRestServerFullMemory.DefinitionTo(Definition: TSynConnectionDefinition);
  41921. begin
  41922. if Definition=nil then
  41923. exit;
  41924. inherited; // set Kind
  41925. Definition.ServerName := StringToUTF8(fFileName);
  41926. if fBinaryFile then
  41927. Definition.DatabaseName := 'binary';
  41928. end;
  41929. procedure TSQLRestServerFullMemory.CreateMissingTables(user_version: cardinal=0;
  41930. Options: TSQLInitializeTableOptions=[]);
  41931. var t: integer;
  41932. begin
  41933. inherited;
  41934. // create any missing static instances
  41935. if integer(fStaticDataCount)<>length(fModel.Tables) then begin
  41936. SetLength(fStorage,length(fModel.Tables));
  41937. for t := fStaticDataCount to high(fModel.Tables) do begin
  41938. fStorage[t] := (StaticDataCreate(fModel.Tables[t]) as TSQLRestStorageInMemory);
  41939. fStorage[t].fStorageLockShouldIncreaseOwnerInternalState := true;
  41940. end;
  41941. fStaticDataCount := length(fModel.Tables);
  41942. end;
  41943. // initialize new tables
  41944. for t := 0 to fStaticDataCount-1 do
  41945. with TSQLRestStorageInMemory(fStaticData[t]) do
  41946. if Count=0 then // emulates TSQLRestServerDB.CreateMissingTables
  41947. StoredClass.InitializeTable(Self,'',Options);
  41948. end;
  41949. destructor TSQLRestServerFullMemory.Destroy;
  41950. begin
  41951. UpdateToFile;
  41952. inherited;
  41953. end;
  41954. procedure TSQLRestServerFullMemory.DropDatabase;
  41955. var t: integer;
  41956. begin
  41957. for t := 0 to fStaticDataCount-1 do
  41958. TSQLRestStorageInMemory(fStaticData[t]).DropValues;
  41959. end;
  41960. procedure TSQLRestServerFullMemory.LoadFromStream(aStream: TStream);
  41961. var JSON: RawUTF8;
  41962. P, TableName, Data: PUTF8Char;
  41963. t: integer;
  41964. wasString: boolean;
  41965. begin
  41966. if aStream=nil then
  41967. exit;
  41968. if fBinaryFile then begin
  41969. if ReadStringFromStream(aStream)=RawUTF8(ClassName)+'00' then
  41970. repeat
  41971. t := Model.GetTableIndex(ReadStringFromStream(aStream));
  41972. until (t<0) or
  41973. not TSQLRestStorageInMemory(fStaticData[t]).LoadFromBinary(aStream);
  41974. end else begin // [{"AuthUser":[{....},{...}]},{"AuthGroup":[{...},{...}]}]
  41975. JSON := StreamToRawByteString(aStream); // assume UTF-8 content
  41976. if JSON='' then
  41977. exit;
  41978. P := pointer(JSON);
  41979. while (P^<>'[') do if P^=#0 then exit else inc(P);
  41980. inc(P);
  41981. repeat
  41982. while (P^<>']') and (P^<>'{') do if P^=#0 then exit else inc(P);
  41983. if P^=']' then break else inc(P);
  41984. TableName := GetJSONField(P,P,@wasString);
  41985. if not wasString or (P=nil) then
  41986. exit;
  41987. t := Model.GetTableIndex(TableName);
  41988. if t<0 then
  41989. exit;
  41990. Data := P;
  41991. P := GotoNextJSONObjectOrArray(P);
  41992. if P=nil then
  41993. break else
  41994. TSQLRestStorageInMemory(fStaticData[t]).LoadFromJSON(Data,P-Data);
  41995. until false;
  41996. end;
  41997. end;
  41998. procedure TSQLRestServerFullMemory.LoadFromFile;
  41999. var S: TFileStream;
  42000. begin
  42001. if (fFileName='') or not FileExists(fFileName) then
  42002. exit;
  42003. DropDatabase;
  42004. S := FileStreamSequentialRead(FileName);
  42005. try
  42006. LoadFromStream(S);
  42007. finally
  42008. S.Free;
  42009. end;
  42010. end;
  42011. procedure TSQLRestServerFullMemory.UpdateToFile;
  42012. const CHARS: array[0..6] of AnsiChar = '[{":,}]';
  42013. var S: TFileStream; // 0123456
  42014. t: integer;
  42015. Modified: boolean;
  42016. Timer: TPrecisionTimer;
  42017. begin
  42018. if (self=nil) or (FileName='') then
  42019. exit;
  42020. Modified := false;
  42021. for t := 0 to fStaticDataCount-1 do
  42022. if TSQLRestStorageInMemory(fStaticData[t]).Modified then begin
  42023. Modified := true;
  42024. break;
  42025. end;
  42026. if not Modified then
  42027. exit;
  42028. Timer.Start;
  42029. S := TFileStream.Create(FileName,fmCreate);
  42030. try
  42031. if fBinaryFile then begin
  42032. WriteStringToStream(S,RawUTF8(ClassName)+'00');
  42033. for t := 0 to fStaticDataCount-1 do
  42034. with TSQLRestStorageInMemory(fStaticData[t]) do begin
  42035. WriteStringToStream(S,fStoredClassRecordProps.SQLTableName);
  42036. SaveToBinary(S);
  42037. end;
  42038. end else begin
  42039. S.Write(CHARS[0],1);
  42040. for t := 0 to fStaticDataCount-1 do
  42041. with TSQLRestStorageInMemory(fStaticData[t]) do begin
  42042. S.Write(CHARS[1],2);
  42043. with fStoredClassRecordProps do
  42044. S.Write(pointer(SQLTableName)^,length(SQLTableName));
  42045. S.Write(CHARS[2],2);
  42046. SaveToJSON(S,true);
  42047. S.Write(CHARS[5],1);
  42048. if t<integer(fStaticDataCount-1) then
  42049. S.Write(CHARS[4],1);
  42050. end;
  42051. S.Write(CHARS[6],1);
  42052. end;
  42053. finally
  42054. S.Free;
  42055. end;
  42056. InternalLog('UpdateToFile done in %',[Timer.Stop],sllDB);
  42057. end;
  42058. function TSQLRestServerFullMemory.EngineExecute(const aSQL: RawUTF8): boolean;
  42059. begin
  42060. result := false; // not implemented in this basic REST server class
  42061. end;
  42062. procedure TSQLRestServerFullMemory.Flush(Ctxt: TSQLRestServerURIContext);
  42063. begin
  42064. if Ctxt.Method=mPUT then begin
  42065. UpdateToFile;
  42066. Ctxt.Success;
  42067. end;
  42068. end;
  42069. function TSQLRestServerFullMemory.GetStorage(aTable: TSQLRecordClass): TSQLRestStorageInMemory;
  42070. var i: cardinal;
  42071. begin
  42072. i := fModel.GetTableIndex(aTable);
  42073. if i>=cardinal(length(fStorage)) then
  42074. result := nil else
  42075. result := fStorage[i];
  42076. end;
  42077. // Engine*() methods will have direct access to static fStorage[])
  42078. function TSQLRestServerFullMemory.EngineAdd(TableModelIndex: integer;
  42079. const SentData: RawUTF8): TID;
  42080. begin
  42081. result := fStorage[TableModelIndex].EngineAdd(TableModelIndex,SentData);
  42082. InternalState := InternalState+1;
  42083. end;
  42084. function TSQLRestServerFullMemory.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
  42085. begin
  42086. result := fStorage[TableModelIndex].EngineRetrieve(TableModelIndex,ID);
  42087. end;
  42088. function TSQLRestServerFullMemory.EngineUpdate(TableModelIndex: integer; ID: TID;
  42089. const SentData: RawUTF8): boolean;
  42090. begin
  42091. result := fStorage[TableModelIndex].EngineUpdate(TableModelIndex,ID,SentData);
  42092. end;
  42093. function TSQLRestServerFullMemory.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
  42094. begin
  42095. result := fStorage[TableModelIndex].EngineDelete(TableModelIndex,ID);
  42096. end;
  42097. function TSQLRestServerFullMemory.EngineDeleteWhere(TableModelIndex: integer;
  42098. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  42099. begin
  42100. result := fStorage[TableModelIndex].EngineDeleteWhere(TableModelIndex,SQLWhere,IDs);
  42101. end;
  42102. function TSQLRestServerFullMemory.EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  42103. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  42104. begin
  42105. result := fStorage[TableModelIndex].EngineRetrieveBlob(TableModelIndex,aID,BlobField,BlobData);
  42106. end;
  42107. function TSQLRestServerFullMemory.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  42108. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  42109. begin
  42110. result := fStorage[TableModelIndex].EngineUpdateBlob(TableModelIndex,aID,BlobField,BlobData);
  42111. end;
  42112. function TSQLRestServerFullMemory.EngineUpdateField(TableModelIndex: integer;
  42113. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  42114. begin
  42115. result := fStorage[TableModelIndex].EngineUpdateField(TableModelIndex,
  42116. SetFieldName,SetValue,WhereFieldName,WhereValue);
  42117. end;
  42118. function TSQLRestServerFullMemory.EngineUpdateFieldIncrement(TableModelIndex: integer;
  42119. ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
  42120. begin
  42121. result := fStorage[TableModelIndex].EngineUpdateFieldIncrement(TableModelIndex,
  42122. ID,FieldName,Increment);
  42123. end;
  42124. // MainEngine*() methods should return error (only access via static fStorage[])
  42125. function TSQLRestServerFullMemory.MainEngineAdd(TableModelIndex: integer;
  42126. const SentData: RawUTF8): TID;
  42127. begin
  42128. result := 0;
  42129. end;
  42130. function TSQLRestServerFullMemory.MainEngineRetrieve(TableModelIndex: integer;
  42131. ID: TID): RawUTF8;
  42132. begin
  42133. result := '';
  42134. end;
  42135. function TSQLRestServerFullMemory.MainEngineList(const SQL: RawUTF8;
  42136. ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
  42137. begin
  42138. result := '';
  42139. end;
  42140. function TSQLRestServerFullMemory.MainEngineUpdate(TableModelIndex: integer; aID: TID;
  42141. const SentData: RawUTF8): boolean;
  42142. begin
  42143. result := false;
  42144. end;
  42145. function TSQLRestServerFullMemory.MainEngineDelete(TableModelIndex: integer; ID: TID): boolean;
  42146. begin
  42147. result := false;
  42148. end;
  42149. function TSQLRestServerFullMemory.MainEngineDeleteWhere(TableModelIndex: integer;
  42150. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  42151. begin
  42152. result := false;
  42153. end;
  42154. function TSQLRestServerFullMemory.MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  42155. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  42156. begin
  42157. result := false;
  42158. end;
  42159. function TSQLRestServerFullMemory.MainEngineUpdateBlob(TableModelIndex: integer; aID: TID;
  42160. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  42161. begin
  42162. result := false;
  42163. end;
  42164. function TSQLRestServerFullMemory.MainEngineUpdateField(TableModelIndex: integer;
  42165. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  42166. begin
  42167. result := false;
  42168. end;
  42169. function TSQLRestServerFullMemory.MainEngineUpdateFieldIncrement(
  42170. TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
  42171. begin
  42172. result := false;
  42173. end;
  42174. { TSQLRestServerRemoteDB }
  42175. constructor TSQLRestServerRemoteDB.Create(aRemoteRest: TSQLRest;
  42176. aHandleUserAuthentication: boolean);
  42177. var i: integer;
  42178. begin
  42179. if aRemoteRest=nil then
  42180. raise EORMException.CreateUTF8('%.Create(nil)',[self]);
  42181. inherited Create(aRemoteRest.Model,aHandleUserAuthentication);
  42182. SetLength(fRemoteTableIndex,Model.TablesMax+1);
  42183. for i := 0 to Model.TablesMax do
  42184. fRemoteTableIndex[i] := aRemoteRest.Model.GetTableIndexExisting(Model.Tables[i]);
  42185. fRemoteRest := aRemoteRest;
  42186. end;
  42187. function TSQLRestServerRemoteDB.EngineAdd(TableModelIndex: integer;
  42188. const SentData: RawUTF8): TID;
  42189. begin
  42190. result := fRemoteRest.EngineAdd(fRemoteTableIndex[TableModelIndex],SentData);
  42191. end;
  42192. function TSQLRestServerRemoteDB.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
  42193. begin
  42194. result := fRemoteRest.EngineDelete(fRemoteTableIndex[TableModelIndex],ID);
  42195. end;
  42196. function TSQLRestServerRemoteDB.EngineDeleteWhere(TableModelIndex: Integer;
  42197. const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
  42198. begin
  42199. result := fRemoteRest.EngineDeleteWhere(fRemoteTableIndex[TableModelIndex],SQLWhere,IDs);
  42200. end;
  42201. function TSQLRestServerRemoteDB.EngineExecute(const aSQL: RawUTF8): boolean;
  42202. begin
  42203. result := fRemoteRest.EngineExecute(aSQL);
  42204. end;
  42205. function TSQLRestServerRemoteDB.EngineList(const SQL: RawUTF8;
  42206. ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
  42207. begin
  42208. result := fRemoteRest.EngineList(SQL,ForceAJAX,ReturnedRowCount);
  42209. end;
  42210. function TSQLRestServerRemoteDB.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
  42211. begin
  42212. result := fRemoteRest.EngineRetrieve(fRemoteTableIndex[TableModelIndex],ID);
  42213. end;
  42214. function TSQLRestServerRemoteDB.EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  42215. BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
  42216. begin
  42217. if (self=nil) or (BlobField=nil) then
  42218. result := false else
  42219. result := fRemoteRest.EngineRetrieveBlob(fRemoteTableIndex[TableModelIndex],aID,BlobField,BlobData);
  42220. end;
  42221. function TSQLRestServerRemoteDB.EngineUpdate(TableModelIndex: integer;
  42222. ID: TID; const SentData: RawUTF8): boolean;
  42223. begin
  42224. result := fRemoteRest.EngineUpdate(fRemoteTableIndex[TableModelIndex],ID,SentData);
  42225. end;
  42226. function TSQLRestServerRemoteDB.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  42227. BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
  42228. begin
  42229. if (self=nil) or (BlobField=nil) then
  42230. result := false else
  42231. result := fRemoteRest.EngineUpdateBlob(fRemoteTableIndex[TableModelIndex],aID,BlobField,BlobData);
  42232. end;
  42233. function TSQLRestServerRemoteDB.EngineUpdateField(TableModelIndex: integer;
  42234. const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
  42235. begin
  42236. result := fRemoteRest.EngineUpdateField(fRemoteTableIndex[TableModelIndex],SetFieldName,SetValue,WhereFieldName,WhereValue);
  42237. end;
  42238. function TSQLRestServerRemoteDB.EngineUpdateFieldIncrement(TableModelIndex: integer;
  42239. ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
  42240. begin
  42241. result := fRemoteRest.EngineUpdateFieldIncrement(fRemoteTableIndex[TableModelIndex],
  42242. ID,FieldName,Increment);
  42243. end;
  42244. function TSQLRestServerRemoteDB.AfterDeleteForceCoherency(TableIndex: integer;
  42245. aID: TID): boolean;
  42246. begin
  42247. result := true; // coherency will be performed on the server side
  42248. end;
  42249. { TSQLRestClient }
  42250. function TSQLRestClient.GetForceBlobTransfert: Boolean;
  42251. var i: integer;
  42252. begin
  42253. result := false;
  42254. if fForceBlobTransfert=nil then
  42255. exit;
  42256. for i := 0 to fModel.fTablesMax do
  42257. if not fForceBlobTransfert[i] then
  42258. exit;
  42259. result := true; // all Tables have BLOB transfert set
  42260. end;
  42261. procedure TSQLRestClient.SetForceBlobTransfert(Value: boolean);
  42262. var i: integer;
  42263. begin
  42264. Finalize(fForceBlobTransfert);
  42265. if Value then begin
  42266. SetLength(fForceBlobTransfert,fModel.fTablesMax+1);
  42267. for i := 0 to fModel.fTablesMax do
  42268. fForceBlobTransfert[i] := true;
  42269. end;
  42270. end;
  42271. function TSQLRestClient.GetForceBlobTransfertTable(aTable: TSQLRecordClass): Boolean;
  42272. begin
  42273. if fForceBlobTransfert=nil then
  42274. result := false else
  42275. result := fForceBlobTransfert[fModel.GetTableIndexExisting(aTable)];
  42276. end;
  42277. procedure TSQLRestClient.SetForceBlobTransfertTable(aTable: TSQLRecordClass; aValue: Boolean);
  42278. var i: integer;
  42279. begin
  42280. i := fModel.GetTableIndexExisting(aTable);
  42281. if fForceBlobTransfert=nil then
  42282. if aValue then
  42283. SetLength(fForceBlobTransfert,fModel.fTablesMax+1) else
  42284. exit; // nothing to set
  42285. fForceBlobTransfert[i] := aValue;
  42286. end;
  42287. function TSQLRestClient.InternalAdd(Value: TSQLRecord; SendData: boolean;
  42288. CustomFields: PSQLFieldBits; ForceID, DoNotAutoComputeFields: boolean): TID;
  42289. begin
  42290. result := inherited InternalAdd(Value,SendData,CustomFields,ForceID,DoNotAutoComputeFields);
  42291. if (result>0) and (fForceBlobTransfert<>nil) and
  42292. fForceBlobTransfert[fModel.GetTableIndexExisting(PSQLRecordClass(Value)^)] then
  42293. UpdateBlobFields(Value);
  42294. end;
  42295. function TSQLRestClient.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
  42296. var dummy: cardinal;
  42297. begin
  42298. if not ClientRetrieve(TableModelIndex,ID,false,dummy,result) then
  42299. result := '';
  42300. end;
  42301. function TSQLRestClient.Retrieve(aID: TID; Value: TSQLRecord;
  42302. ForUpdate: boolean=false): boolean;
  42303. var Resp: RawUTF8;
  42304. TableIndex: integer;
  42305. begin
  42306. result := false;
  42307. if (self=nil) or (aID<=0) or (Value=nil) then
  42308. exit;
  42309. TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^);
  42310. if ForUpdate then begin
  42311. if not Model.Lock(TableIndex,aID) then
  42312. exit; // error marking as locked by the client
  42313. end else begin
  42314. Resp := fCache.Retrieve(TableIndex,aID);
  42315. if Resp<>'' then begin
  42316. Value.FillFrom(Resp);
  42317. Value.fID := aID; // JSON object may not contain the ID
  42318. result := true;
  42319. exit; // fast retrieved from internal Client cache (BLOBs ignored)
  42320. end;
  42321. end;
  42322. try
  42323. if ClientRetrieve(TableIndex,aID,ForUpdate,Value.fInternalState,Resp) then begin
  42324. if not ForUpdate then
  42325. fCache.Notify(TableIndex,aID,Resp,soSelect);
  42326. Value.FillFrom(Resp);
  42327. Value.fID := aID; // JSON object may not contain the ID
  42328. if (fForceBlobTransfert<>nil) and fForceBlobTransfert[TableIndex] then
  42329. result := RetrieveBlobFields(Value) else
  42330. result := true;
  42331. ForUpdate := false; // any exception shall unlock the record
  42332. end;
  42333. finally
  42334. if ForUpdate then
  42335. Model.UnLock(TableIndex,aID);
  42336. end;
  42337. end;
  42338. function TSQLRestClient.Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits;
  42339. DoNotAutoComputeFields: boolean): boolean;
  42340. begin
  42341. result := BeforeUpdateEvent(Value) and
  42342. inherited Update(Value,CustomFields,DoNotAutoComputeFields);
  42343. if result then begin
  42344. if (fForceBlobTransfert<>nil) and IsZero(CustomFields) and
  42345. fForceBlobTransfert[Model.GetTableIndexExisting(PSQLRecordClass(Value)^)] then
  42346. result := UpdateBlobFields(Value);
  42347. if result and assigned(OnRecordUpdate) then
  42348. OnRecordUpdate(Value);
  42349. end;
  42350. end;
  42351. function TSQLRestClient.BeforeUpdateEvent(Value: TSQLRecord): Boolean;
  42352. begin
  42353. Result := true; // by default, just allow the update to proceed
  42354. end;
  42355. function TSQLRestClient.Refresh(aID: TID; Value: TSQLRecord;
  42356. var Refreshed: boolean): boolean;
  42357. var Resp, Original: RawUTF8;
  42358. begin
  42359. result := false;
  42360. if (aID>0) and (self<>nil) and (Value<>nil) then
  42361. if ClientRetrieve(Model.GetTableIndexExisting(PSQLRecordClass(Value)^),aID,False,
  42362. Value.fInternalState,Resp) then begin
  42363. Original := Value.GetJSONValues(IsNotAjaxJSON(pointer(Resp)),true,soSelect);
  42364. Resp := trim(Resp);
  42365. if (Resp<>'') and (Resp[1]='[') then // '[{....}]' -> '{...}'
  42366. Resp := copy(Resp,2,length(Resp)-2);
  42367. if Original<>Resp then begin // did the content really change?
  42368. Refreshed := true;
  42369. Value.FillFrom(Resp);
  42370. end;
  42371. result := true;
  42372. end;
  42373. end;
  42374. procedure TSQLRestClient.Commit(SessionID: cardinal; RaiseException: boolean);
  42375. begin
  42376. inherited Commit(SessionID,RaiseException);
  42377. end;
  42378. function TSQLRestClient.TransactionBegin(aTable: TSQLRecordClass;
  42379. SessionID: cardinal): boolean;
  42380. begin
  42381. result := inherited TransactionBegin(aTable,SessionID);
  42382. end;
  42383. procedure TSQLRestClient.RollBack(SessionID: cardinal);
  42384. begin
  42385. inherited;
  42386. end;
  42387. function TSQLRestClient.ListFmt(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8;
  42388. const SQLWhereFormat: RawUTF8; const Args: array of const): TSQLTableJSON;
  42389. begin
  42390. result := List(Tables,SQLSelect,FormatUTF8(SQLWhereFormat,Args));
  42391. end;
  42392. function TSQLRestClient.ListFmt(const Tables: array of TSQLRecordClass;
  42393. const SQLSelect: RawUTF8; const SQLWhereFormat: RawUTF8;
  42394. const Args, Bounds: array of const): TSQLTableJSON;
  42395. begin
  42396. result := List(Tables,SQLSelect,FormatUTF8(SQLWhereFormat,Args,Bounds));
  42397. end;
  42398. function TSQLRestClient.RTreeMatch(DataTable: TSQLRecordClass;
  42399. const DataTableBlobFieldName: RawUTF8; RTreeTable: TSQLRecordRTreeClass;
  42400. const DataTableBlobField: RawByteString; var DataID: TIDDynArray): boolean;
  42401. var Blob: PPropInfo;
  42402. Res: TSQLTableJSON;
  42403. B: TSQLRecordTreeCoords;
  42404. Where: RawUTF8;
  42405. Data, RTree: TSQLRecordProperties;
  42406. i: integer;
  42407. begin
  42408. result := false;
  42409. if (self=nil) or (DataTable=nil) or (RTreeTable=nil) or (DataTableBlobField='') then
  42410. exit;
  42411. RTree := RTreeTable.RecordProps;
  42412. Data := DataTable.RecordProps;
  42413. Blob := Data.BlobFieldPropFromRawUTF8(DataTableBlobFieldName);
  42414. if Blob=nil then
  42415. exit;
  42416. for i := 0 to (RTree.Fields.Count shr 1)-1 do
  42417. Where := FormatUTF8('%% >= :(%): AND % <= :(%): AND ',
  42418. [Where,RTree.Fields.List[i*2].Name,B[i].Min,RTree.Fields.List[i*2+1].Name,
  42419. B[i].Max]);
  42420. RTreeTable.BlobToCoord(DataTableBlobField[1],B);
  42421. Res := ListFmt([DataTable,RTreeTable],Data.SQLTableName+'.RowID',
  42422. 'WHERE %.RowID=%.RowID AND %%(%,:(%):);',
  42423. [Data.SQLTableName,RTree.SQLTableName,Where,
  42424. RTreeTable.RTreeSQLFunctionName,Data.SQLTableName,
  42425. BinToBase64WithMagic(DataTableBlobField)]);
  42426. if Res<>nil then
  42427. try
  42428. if (Res.FieldCount<>1) or (Res.fRowCount<=0) then
  42429. exit;
  42430. Res.GetRowValues(0,TInt64DynArray(DataID));
  42431. result := true;
  42432. finally
  42433. Res.Free;
  42434. end;
  42435. end;
  42436. function TSQLRestClient.ServiceContainer: TServiceContainer;
  42437. begin
  42438. if fServices=nil then
  42439. fServices := TServiceContainerClient.Create(self);
  42440. result := fServices;
  42441. end;
  42442. { TSQLRecordLog }
  42443. destructor TSQLRecordLog.Destroy;
  42444. begin
  42445. fLogTableWriter.Free;
  42446. fLogTableStorage.Free;
  42447. inherited;
  42448. end;
  42449. constructor TSQLRecordLog.CreateFrom(OneLog: TSQLRecord; const aJSON: RawUTF8);
  42450. var L,FieldCount: integer;
  42451. P: PUTF8Char;
  42452. begin
  42453. inherited Create;
  42454. L := length(aJSON);
  42455. if (L<10) or (Copy(aJSON,L-1,2)<>']}') then
  42456. exit;
  42457. fLogTableStorage := THeapMemoryStream.Create;
  42458. fLogTableWriter := OneLog.RecordProps.CreateJSONWriter(
  42459. fLogTableStorage,false,true,ALL_ACCESS_RIGHTS,0);
  42460. fLogTableWriter.FlushToStream;
  42461. P := pointer(aJSON);
  42462. if not CompareMem(fLogTableStorage.Memory,P,fLogTableStorage.Position) or
  42463. not IsNotExpandedBuffer(P,P+length(aJSON),FieldCount,fLogTableRowCount) or
  42464. (fLogTableRowCount<0) then begin
  42465. // field format changed or invalid
  42466. FreeAndNil(fLogTableWriter);
  42467. FreeAndNil(fLogTableStorage);
  42468. exit;
  42469. end;
  42470. fLogTableStorage.Seek(0,soFromBeginning);
  42471. fLogTableStorage.Write(Pointer(aJSON)^,L-2);
  42472. end;
  42473. procedure TSQLRecordLog.Log(OneLog: TSQLRecord);
  42474. begin
  42475. if OneLog=nil then
  42476. exit;
  42477. // simulate adding a row: compute new ID
  42478. inc(OneLog.fID);
  42479. // adding a row, in not-expanded format
  42480. if not Assigned(fLogTableStorage) then begin
  42481. fLogTableStorage := THeapMemoryStream.Create;
  42482. fLogTableWriter := OneLog.RecordProps.CreateJSONWriter(
  42483. fLogTableStorage,false,true,ALL_ACCESS_RIGHTS,0);
  42484. fLogTableRowCount := 1;
  42485. end else begin
  42486. fLogTableWriter.Add(',');
  42487. if (fMaxLogTableRowCount<>0) and (fLogTableRowCount>=fMaxLogTableRowCount) then
  42488. fLogTableWriter.TrimFirstRow else
  42489. inc(fLogTableRowCount);
  42490. end;
  42491. OneLog.GetJSONValues(fLogTableWriter)
  42492. end;
  42493. function TSQLRecordLog.LogCurrentPosition: integer;
  42494. begin
  42495. if not Assigned(fLogTableStorage) then
  42496. result := 0 else begin
  42497. fLogTableWriter.FlushToStream;
  42498. result := fLogTableStorage.Position;
  42499. end;
  42500. end;
  42501. function TSQLRecordLog.LogTableJSON: RawUTF8;
  42502. begin
  42503. result := LogTableJSONFrom(0);
  42504. end;
  42505. function TSQLRecordLog.LogTableJSONFrom(StartPosition: integer): RawUTF8;
  42506. var JSONStart: RawUTF8;
  42507. Data: PAnsiChar;
  42508. begin
  42509. if not Assigned(fLogTableStorage) or (StartPosition<0) then
  42510. result := '' else begin
  42511. fLogTableWriter.FlushToStream;
  42512. Data := fLogTableStorage.Memory;
  42513. SetString(result,Data+StartPosition,fLogTableStorage.Position-StartPosition);
  42514. // format as valid not expanded JSON table content:
  42515. if StartPosition<>0 then begin
  42516. SetString(JSONStart,Data,fLogTableWriter.StartDataPosition);
  42517. result := JSONStart+result;
  42518. end;
  42519. result := result+']}';
  42520. end;
  42521. end;
  42522. { RecordRef }
  42523. function RecordReference(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID): TRecordReference;
  42524. begin
  42525. if aID=0 then
  42526. result := 0 else begin
  42527. result := Model.GetTableIndexExisting(aTable);
  42528. if result>63 then // TRecordReference handle up to 64=1 shl 6 tables
  42529. result := 0 else
  42530. inc(result,aID shl 6); // 64=1 shl 6
  42531. end;
  42532. end;
  42533. function RecordReference(aTableIndex: cardinal; aID: TID): TRecordReference;
  42534. begin
  42535. if (aID=0) or (aTableIndex>63) then
  42536. result := 0 else
  42537. result := aTableIndex+aID shl 6;
  42538. end;
  42539. procedure RecordRefToID(var aArray: TInt64DynArray);
  42540. var i: Integer;
  42541. begin
  42542. for i := 0 to high(aArray) do
  42543. aArray[i] := aArray[i] shr 6;
  42544. end;
  42545. procedure RecordRef.From(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID);
  42546. begin
  42547. Value := Model.GetTableIndexExisting(aTable);
  42548. if Value>63 then // TRecordReference handle up to 64=1 shl 6 tables
  42549. Value := 0 else
  42550. inc(Value,aID shl 6); // 64=1 shl 6
  42551. end;
  42552. function RecordRef.ID: TID;
  42553. begin
  42554. result := Value shr 6; // 64=1 shl 6
  42555. end;
  42556. function RecordRef.Table(Model: TSQLModel): TSQLRecordClass;
  42557. var V: integer;
  42558. begin
  42559. if (Model=nil) or (Value=0) then
  42560. result := nil else begin
  42561. V := Value and 63;
  42562. if V>Model.TablesMax then
  42563. result := nil else
  42564. result := Model.Tables[V];
  42565. end;
  42566. end;
  42567. function RecordRef.TableIndex: integer;
  42568. begin
  42569. result := Value and 63;
  42570. end;
  42571. function RecordRef.Text(Model: TSQLModel): RawUTF8;
  42572. var aTable: TSQLRecordClass;
  42573. begin
  42574. if ((Value shr 6)=0) then
  42575. // Value=0 or no valid ID
  42576. result := '' else begin
  42577. aTable := Table(Model);
  42578. if aTable=nil then
  42579. result := '' else
  42580. result := Model.TableProps[Value and 63].Props.SQLTableName+
  42581. ' '+Int64ToUtf8(Value shr 6);
  42582. end;
  42583. end;
  42584. function RecordRef.Text(Rest: TSQLRest): RawUTF8;
  42585. var T: TSQLRecordClass;
  42586. aID: TID;
  42587. begin
  42588. result := '';
  42589. if ((Value shr 6)=0) or (Rest=nil) then
  42590. exit;
  42591. T := Table(Rest.Model);
  42592. if T=nil then
  42593. exit;
  42594. aID := ID;
  42595. with Rest.Model.TableProps[Value and 63].Props do
  42596. if aID<=0 then
  42597. result := SQLTableName else begin
  42598. result := Rest.MainFieldValue(T,aID,true);
  42599. if result='' then
  42600. FormatUTF8('% %',[SQLTableName,aID],result) else
  42601. result := FormatUTF8('% "%"',[SQLTableName,result]);
  42602. end;
  42603. end;
  42604. { TSQLLocks }
  42605. function TSQLLocks.isLocked(aID: TID): boolean;
  42606. begin
  42607. result := (@self<>nil) and (Count<>0) and (aID<>0) and
  42608. Int64ScanExists(pointer(IDs),Count,aID);
  42609. end;
  42610. function TSQLLocks.Lock(aID: TID): boolean;
  42611. var P: PInt64;
  42612. begin
  42613. if (@self=nil) or (aID=0) then
  42614. // void or full
  42615. result := false else begin
  42616. P := Int64Scan(pointer(IDs),Count,aID);
  42617. if P<>nil then
  42618. // already locked
  42619. result := false else begin
  42620. // add to ID[] and Ticks[]
  42621. P := Int64Scan(pointer(IDs),Count,0);
  42622. if P=nil then begin
  42623. // no free entry -> add at the end
  42624. if Count>=length(IDs) then begin
  42625. SetLength(IDs,Count+512);
  42626. SetLength(Ticks64s,Count+512);
  42627. end;
  42628. IDs[Count] := aID;
  42629. Ticks64s[Count] := GetTickCount64;
  42630. inc(Count);
  42631. end else begin
  42632. // store at free entry
  42633. P^ := aID;
  42634. Ticks64s[(PtrUInt(P)-PtrUInt(IDs))shr 3] := GetTickCount64;
  42635. end;
  42636. result := true;
  42637. end;
  42638. end;
  42639. end;
  42640. procedure TSQLLocks.PurgeOlderThan(MinutesFromNow: cardinal);
  42641. var LastOK64: Int64;
  42642. i, LastEntry: integer;
  42643. begin
  42644. if (@self=nil) or (Count=0) then
  42645. exit; // nothing to purge
  42646. LastOK64 := GetTickCount64-MinutesFromNow*(1000*60); // GetTickCount64() unit is ms
  42647. LastEntry := -1;
  42648. for i := 0 to Count-1 do
  42649. if IDs[i]<>0 then
  42650. if Ticks64s[i]<LastOK64 then // too old?
  42651. IDs[i] := 0 else // 0 frees entry
  42652. LastEntry := i; // refresh last existing entry
  42653. Count := LastEntry+1; // update count (may decrease list length)
  42654. end;
  42655. function TSQLLocks.UnLock(aID: TID): boolean;
  42656. var P: PInt64;
  42657. begin
  42658. if (@self=nil) or (Count=0) or (aID=0) then
  42659. result := false else begin
  42660. P := Int64Scan(pointer(IDs),Count,aID);
  42661. if P=nil then
  42662. result := false else begin
  42663. P^ := 0; // 0 marks free entry
  42664. if ((PtrUInt(P)-PtrUInt(IDs))shr 3>=PtrUInt(Count-1)) then
  42665. dec(Count); // freed last entry -> decrease list length
  42666. result := true;
  42667. end;
  42668. end;
  42669. end;
  42670. procedure CopyObject(aFrom, aTo: TObject);
  42671. var P,P2: PPropInfo;
  42672. i: integer;
  42673. C,C2: TClass;
  42674. begin
  42675. if (aFrom=nil) or (aTo=nil) then
  42676. exit;
  42677. {$ifndef LVCL}
  42678. if aFrom.InheritsFrom(TCollection) then begin
  42679. CopyCollection(TCollection(aFrom),TCollection(aTo));
  42680. exit;
  42681. end;
  42682. {$endif}
  42683. if aFrom.InheritsFrom(TStrings) then begin
  42684. if aTo.InheritsFrom(TStrings) then
  42685. CopyStrings(TStrings(aFrom),TStrings(aTo));
  42686. exit;
  42687. end;
  42688. C := aFrom.ClassType;
  42689. C2 := aTo.ClassType;
  42690. if C2.InheritsFrom(C) then
  42691. repeat // fast process of inherited PPropInfo
  42692. for i := 1 to InternalClassPropInfo(C,P) do begin
  42693. P^.CopyValue(aFrom,aTo);
  42694. P := P^.Next;
  42695. end;
  42696. C := C.ClassParent;
  42697. until C=TObject else
  42698. if C.InheritsFrom(C2) then
  42699. repeat // fast process of inherited PPropInfo
  42700. for i := 1 to InternalClassPropInfo(C2,P) do begin
  42701. P^.CopyValue(aFrom,aTo);
  42702. P := P^.Next;
  42703. end;
  42704. C2 := C2.ClassParent;
  42705. until C2=TObject else
  42706. repeat // slower lookup by property name
  42707. for i := 1 to InternalClassPropInfo(C,P) do begin
  42708. P2 := ClassFieldPropWithParents(C2,P^.Name);
  42709. if P2<>nil then
  42710. P^.CopyValue(aFrom,aTo,P2);
  42711. P := P^.Next;
  42712. end;
  42713. C := C.ClassParent;
  42714. until C=TObject;
  42715. end;
  42716. function CopyObject(aFrom: TObject): TObject;
  42717. var DInst: TClassInstance;
  42718. begin
  42719. if aFrom=nil then begin
  42720. result := nil;
  42721. exit;
  42722. end;
  42723. DInst.Init(aFrom.ClassType);
  42724. result := DInst.CreateNew;
  42725. try
  42726. CopyObject(aFrom,result);
  42727. except
  42728. FreeAndNil(result); // avoid memory leak if error during new instance copy
  42729. end;
  42730. end;
  42731. {$ifndef LVCL}
  42732. procedure CopyCollection(Source, Dest: TCollection);
  42733. var i: integer;
  42734. begin
  42735. if (Source=nil) or (Dest=nil) or (Source.ClassType<>Dest.ClassType) then
  42736. exit;
  42737. Dest.BeginUpdate;
  42738. try
  42739. Dest.Clear;
  42740. for i := 0 to Source.Count-1 do
  42741. CopyObject(Source.Items[i],Dest.Add); // Assign() fails for most objects
  42742. finally
  42743. Dest.EndUpdate;
  42744. end;
  42745. end;
  42746. {$endif}
  42747. procedure CopyStrings(Source, Dest: TStrings);
  42748. begin
  42749. if (Source=nil) or (Dest=nil) then
  42750. exit;
  42751. {$ifdef LVCL}
  42752. Dest.Clear;
  42753. Dest.AddStrings(Source);
  42754. {$else}
  42755. Dest.Assign(Source);
  42756. {$endif}
  42757. end;
  42758. procedure WriteObject(Value: TObject; var IniContent: RawUTF8; const Section: RawUTF8;
  42759. const SubCompName: RawUTF8=''); overload;
  42760. var P: PPropInfo;
  42761. i, V: integer;
  42762. Obj: TObject;
  42763. tmp: RawUTF8;
  42764. begin
  42765. if Value=nil then
  42766. exit;
  42767. for i := 1 to InternalClassPropInfo(Value.ClassType,P) do begin
  42768. case P^.PropType^.Kind of
  42769. tkInt64{$ifdef FPC}, tkQWord{$endif}:
  42770. UpdateIniEntry(IniContent,Section,SubCompName+ToUTF8(P^.Name),
  42771. Int64ToUtf8(P^.GetInt64Prop(Value)));
  42772. {$ifdef FPC}tkBool,{$endif} tkEnumeration, tkSet, tkInteger: begin
  42773. V := P^.GetOrdProp(Value);
  42774. //if V<>P^.Default then NO DEFAULT: update INI -> must override previous
  42775. UpdateIniEntry(IniContent,Section,SubCompName+ToUTF8(P^.Name),
  42776. Int32ToUtf8(V));
  42777. end;
  42778. {$ifdef HASVARUSTRING}tkUString,{$endif} {$ifdef FPC}tkAString,{$endif}
  42779. tkLString, tkWString: begin
  42780. P^.GetLongStrValue(Value,tmp);
  42781. UpdateIniEntry(IniContent,Section,SubCompName+ToUTF8(P^.Name),tmp);
  42782. end;
  42783. tkClass:
  42784. if Section='' then begin // recursive call works only as plain object
  42785. Obj := P^.GetObjProp(Value);
  42786. if (Obj<>nil) and Obj.InheritsFrom(TPersistent) then
  42787. WriteObject(Value,IniContent,Section,SubCompName+ToUTF8(P^.Name)+'.');
  42788. end;
  42789. // tkString (shortstring) and tkInterface are not handled
  42790. end;
  42791. P := P^.Next;
  42792. end;
  42793. end;
  42794. function WriteObject(Value: TObject): RawUTF8; overload;
  42795. begin
  42796. if Value<>nil then
  42797. with TIniWriter.CreateOwnedStream do
  42798. try
  42799. WriteObject(Value,'');
  42800. SetText(result);
  42801. finally
  42802. Free;
  42803. end else
  42804. result := '';
  42805. end;
  42806. function ObjectEquals(Value1,Value2: TObject; ignoreGetterFields: boolean): boolean;
  42807. var i: integer;
  42808. C1,C2: TClass;
  42809. P1,P2: PPropInfo;
  42810. begin
  42811. if (Value1=nil) or (Value2=nil) then
  42812. result := Value1=Value2 else
  42813. if Value1.InheritsFrom(TSQLRecord) and Value2.InheritsFrom(TSQLRecord) then
  42814. result := TSQLRecord(Value1).SameValues(TSQLRecord(Value2)) else begin
  42815. result := false;
  42816. C1 := Value1.ClassType;
  42817. C2 := Value2.ClassType;
  42818. repeat
  42819. for i := 1 to InternalClassPropInfo(C1,P1) do begin
  42820. if (not ignoreGetterFields) or P1^.GetterIsField then
  42821. if C2<>C1 then begin
  42822. P2 := ClassFieldPropWithParents(C2,P1^.Name);
  42823. if (P2=nil) or not P1^.SameValue(Value1,P2,Value2) then
  42824. exit;
  42825. end else
  42826. if not P1^.SameValue(Value1,P1,Value2) then
  42827. exit;
  42828. P1 := P1^.Next;
  42829. end;
  42830. C1 := C1.ClassParent;
  42831. until C1=nil;
  42832. result := true;
  42833. end;
  42834. end;
  42835. function ObjectToJSONDebug(Value: TObject): RawUTF8;
  42836. begin
  42837. if Value=nil then
  42838. result := 'null' else
  42839. if Value.InheritsFrom(Exception) and not Value.InheritsFrom(ESynException) then
  42840. result := FormatUTF8('{"%":?}',[Value],[Exception(Value).Message],True) else
  42841. result := ObjectToJSON(Value,
  42842. [woDontStoreDefault,woHumanReadable,woStoreClassName,woStorePointer]);
  42843. end;
  42844. function ObjectToVariantDebug(Value: TObject): variant;
  42845. var json: RawUTF8;
  42846. begin
  42847. VarClear(result);
  42848. json := ObjectToJSONDebug(Value);
  42849. PDocVariantData(@result)^.InitJSONInPlace(pointer(json),JSON_OPTIONS_FAST);
  42850. end;
  42851. procedure _ObjAddProps(Value: TObject; var Obj: variant);
  42852. var v: variant;
  42853. begin
  42854. ObjectToVariant(Value,v,[woDontStoreDefault]);
  42855. _ObjAddProps(v,Obj);
  42856. end;
  42857. function ObjectToVariantDebug(Value: TObject;
  42858. const ContextFormat: RawUTF8; const ContextArgs: array of const;
  42859. const ContextName: RawUTF8): variant;
  42860. begin
  42861. _Json(ObjectToJSONDebug(Value),result,JSON_OPTIONS_FAST);
  42862. if ContextFormat<>'' then
  42863. if ContextFormat[1]='{' then
  42864. _ObjAddProps([ContextName,_JsonFastFmt(ContextFormat,[],ContextArgs)],result) else
  42865. _ObjAddProps([ContextName,FormatUTF8(ContextFormat,ContextArgs)],result);
  42866. end;
  42867. var
  42868. JSONCustomParsers: array of record
  42869. Kind: TClass;
  42870. Reader: TJSONSerializerCustomReader;
  42871. Writer: TJSONSerializerCustomWriter;
  42872. end;
  42873. type
  42874. TJSONCustomParserExpectedDirection = (cpRead, cpWrite);
  42875. TJSONCustomParserExpectedDirections = set of TJSONCustomParserExpectedDirection;
  42876. function JSONCustomParsersIndex(aClass: TClass;
  42877. aExpectedReadWriteTypes: TJSONCustomParserExpectedDirections): integer;
  42878. {$ifdef HASINLINE}inline;{$endif}
  42879. begin
  42880. if JSONCustomParsers<>nil then
  42881. for result := 0 to length(JSONCustomParsers)-1 do
  42882. with JSONCustomParsers[result] do
  42883. if Kind=aClass then
  42884. if ((cpRead in aExpectedReadWriteTypes) and not Assigned(Reader)) or
  42885. ((cpWrite in aExpectedReadWriteTypes) and not Assigned(Writer)) then
  42886. break // any (un)serializer callbacks missing
  42887. else
  42888. exit; // found with appropriate (un)serializers callbacks
  42889. result := -1;
  42890. end;
  42891. class procedure TJSONSerializer.RegisterCustomSerializer(aClass: TClass;
  42892. aReader: TJSONSerializerCustomReader; aWriter: TJSONSerializerCustomWriter);
  42893. var i: integer;
  42894. begin
  42895. i := JSONCustomParsersIndex(aClass,[]);
  42896. if i<0 then begin
  42897. i := length(JSONCustomParsers);
  42898. SetLength(JSONCustomParsers,i+1);
  42899. end;
  42900. with JSONCustomParsers[i] do begin
  42901. Kind := aClass;
  42902. Writer := aWriter;
  42903. Reader := aReader;
  42904. end;
  42905. end;
  42906. constructor TJSONSerializerRegisteredClassAbstract.Create;
  42907. begin
  42908. inherited Create;
  42909. fSafe.Init;
  42910. end;
  42911. destructor TJSONSerializerRegisteredClassAbstract.Destroy;
  42912. begin
  42913. inherited;
  42914. fSafe.Done;
  42915. end;
  42916. function TJSONSerializerRegisteredClass.Find(JSON: PUTF8Char; AndRegisterClass: boolean): TClass;
  42917. var token: shortstring;
  42918. ClassNameValue: PUTF8Char;
  42919. ClassNameLen: integer;
  42920. begin // at input, JSON^='{'
  42921. result := nil;
  42922. if self=nil then
  42923. exit;
  42924. inc(JSON);
  42925. GetJSONPropName(JSON,token);
  42926. if (JSON=nil) or not IdemPropName('ClassName',token) then
  42927. exit; // we expect woStoreClassName option to have been used
  42928. if JSONRetrieveStringField(JSON,ClassNameValue,ClassNameLen,false)=nil then
  42929. exit; //invalid JSON string value
  42930. fSafe.Lock;
  42931. try
  42932. if (fLastClass<>nil) and
  42933. IdemPropName(PShortString(PPointer(PtrInt(fLastClass)+vmtClassName)^)^,
  42934. ClassNameValue,ClassNameLen) then begin
  42935. result := fLastClass; // for speed-up e.g. within a loop
  42936. exit;
  42937. end;
  42938. result := Find(ClassNameValue,ClassNameLen);
  42939. if result=nil then begin // not registered here -> try from Classes.pas
  42940. {$ifndef LVCL}
  42941. if AndRegisterClass then
  42942. result := FindClass(UTF8DecodeToString(ClassNameValue,ClassNameLen));
  42943. if result=nil then
  42944. {$endif}
  42945. exit; // unknown type
  42946. end;
  42947. fLastClass := result;
  42948. finally
  42949. fSafe.UnLock;
  42950. end;
  42951. end;
  42952. procedure TJSONSerializerRegisteredClass.AddOnce(aItemClass: TClass);
  42953. begin
  42954. fSafe.Lock;
  42955. try
  42956. if not PtrUIntScanExists(pointer(List),Count,PtrUInt(aItemClass)) then
  42957. Add(aItemClass);
  42958. finally
  42959. fSafe.UnLock;
  42960. end;
  42961. end;
  42962. function TJSONSerializerRegisteredClass.Find(aClassName: PUTF8Char; aClassNameLen: integer): TClass;
  42963. var i: integer;
  42964. begin
  42965. result := nil;
  42966. fSafe.Lock;
  42967. try
  42968. for i := 0 to Count-1 do
  42969. // new TObject.ClassName is UnicodeString (since Delphi 20009) -> inline code
  42970. // with vmtClassName = UTF-8 encoded text stored in a shortstring = -44
  42971. if IdemPropName(PShortString(PPointer(PtrInt(List[i])+vmtClassName)^)^,
  42972. aClassName,aClassNameLen) then begin
  42973. result := List[i];
  42974. exit;
  42975. end;
  42976. finally
  42977. fSafe.UnLock;
  42978. end;
  42979. end;
  42980. {$ifndef LVCL}
  42981. type
  42982. TJSONSerializerRegisteredCollection = class(TJSONSerializerRegisteredClassAbstract)
  42983. protected
  42984. public
  42985. procedure AddOnce(aCollection: TCollectionClass; aItem: TCollectionItemClass);
  42986. function Find(aCollClassName: PUTF8Char; aCollClassNameLen: integer): TCollectionItemClass; overload;
  42987. function Find(aCollection: TCollectionClass): TCollectionItemClass; overload;
  42988. end;
  42989. function TJSONSerializerRegisteredCollection.Find(aCollection: TCollectionClass): TCollectionItemClass;
  42990. var i: integer;
  42991. begin
  42992. result := nil;
  42993. if self=nil then
  42994. exit;
  42995. fSafe.Lock;
  42996. try
  42997. for i := 0 to (Count shr 1)-1 do
  42998. if TClass(List[i*2])=aCollection then begin
  42999. result := List[i*2+1];
  43000. exit;
  43001. end;
  43002. finally
  43003. fSafe.UnLock;
  43004. end;
  43005. end;
  43006. procedure TJSONSerializerRegisteredCollection.AddOnce(aCollection: TCollectionClass; aItem: TCollectionItemClass);
  43007. begin
  43008. if (self=nil) or (Find(aCollection)<>nil) then
  43009. exit;
  43010. fSafe.Lock;
  43011. try
  43012. Add(aCollection);
  43013. Add(aItem);
  43014. finally
  43015. fSafe.UnLock;
  43016. end;
  43017. end;
  43018. function TJSONSerializerRegisteredCollection.Find(aCollClassName: PUTF8Char;
  43019. aCollClassNameLen: integer): TCollectionItemClass;
  43020. var i: integer;
  43021. begin
  43022. result := nil;
  43023. fSafe.Lock;
  43024. try
  43025. for i := 0 to (Count shr 1)-1 do
  43026. // new TObject.ClassName is UnicodeString (since Delphi 20009) -> inline code
  43027. // with vmtClassName = UTF-8 encoded text stored in a shortstring = -44
  43028. if IdemPropName(PShortString(PPointer(PtrInt(List[i*2])+vmtClassName)^)^,
  43029. aCollClassName,aCollClassNameLen) then begin
  43030. result := List[i*2+1];
  43031. exit;
  43032. end;
  43033. finally
  43034. fSafe.UnLock;
  43035. end;
  43036. end;
  43037. var
  43038. JSONSerializerRegisteredCollection: TJSONSerializerRegisteredCollection=nil;
  43039. class procedure TJSONSerializer.RegisterCollectionForJSON(aCollection: TCollectionClass;
  43040. aItem: TCollectionItemClass);
  43041. begin
  43042. if JSONSerializerRegisteredCollection=nil then
  43043. GarbageCollectorFreeAndNil(JSONSerializerRegisteredCollection,
  43044. TJSONSerializerRegisteredCollection.Create);
  43045. JSONSerializerRegisteredCollection.AddOnce(aCollection,aItem);
  43046. RegisterClassForJSON([aCollection,aItem]);
  43047. end;
  43048. {$endif LVCL}
  43049. class procedure TJSONSerializer.RegisterClassForJSON(aItemClass: TClass);
  43050. begin
  43051. if JSONSerializerRegisteredClass=nil then
  43052. GarbageCollectorFreeAndNil(JSONSerializerRegisteredClass,
  43053. TJSONSerializerRegisteredClass.Create);
  43054. JSONSerializerRegisteredClass.AddOnce(aItemClass);
  43055. end;
  43056. class procedure TJSONSerializer.RegisterClassForJSON(const aItemClass: array of TClass);
  43057. var i: integer;
  43058. begin
  43059. for i := 0 to high(aItemClass) do
  43060. RegisterClassForJSON(aItemClass[i]);
  43061. end;
  43062. class procedure TJSONSerializer.RegisterObjArrayForJSON(aDynArray: PTypeInfo;
  43063. aItem: TClass);
  43064. var serializer: ^TObjArraySerializer;
  43065. begin
  43066. if (aItem=nil) or (aDynArray^.DynArrayItemSize<>sizeof(TObject)) then
  43067. raise EModelException.CreateUTF8(
  43068. 'Invalid %.RegisterObjArrayForJSON(TypeInfo(%),%)',[self,aDynArray^.Name,aItem]);
  43069. if ObjArraySerializers=nil then
  43070. GarbageCollectorFreeAndNil(ObjArraySerializers,TPointerClassHash.Create);
  43071. serializer := pointer(ObjArraySerializers.TryAdd(aDynArray));
  43072. if serializer=nil then
  43073. exit; // avoid duplicate
  43074. serializer^ := TObjArraySerializer.Create(aDynArray);
  43075. serializer^.Instance.Init(aItem);
  43076. TTextWriter.RegisterCustomJSONSerializer(
  43077. aDynArray,serializer^.CustomReader,serializer^.CustomWriter);
  43078. end;
  43079. class function TJSONSerializer.RegisterObjArrayFindType(aDynArray: PTypeInfo): PClassInstance;
  43080. var serializer: TPointerClassHashed;
  43081. begin
  43082. serializer := ObjArraySerializers.Find(aDynArray);
  43083. if serializer=nil then
  43084. result := nil else
  43085. result := @TObjArraySerializer(serializer).Instance;
  43086. end;
  43087. class procedure TJSONSerializer.RegisterObjArrayForJSON(
  43088. const aDynArrayClassPairs: array of const);
  43089. var n,i: integer;
  43090. begin
  43091. n := length(aDynArrayClassPairs);
  43092. if (n=0) or (n and 1=1) then
  43093. exit;
  43094. n := n shr 1;
  43095. if n=0 then
  43096. exit;
  43097. for i := 0 to n-1 do
  43098. if (aDynArrayClassPairs[i*2].VType<>vtPointer) or
  43099. (aDynArrayClassPairs[i*2+1].VType<>vtClass) then
  43100. raise EParsingException.Create('RegisterObjArrayForJSON[?]') else
  43101. RegisterObjArrayForJSON(
  43102. aDynArrayClassPairs[i*2].VPointer,aDynArrayClassPairs[i*2+1].VClass);
  43103. end;
  43104. function JSONToNewObject(var From: PUTF8Char; var Valid: boolean;
  43105. Options: TJSONToObjectOptions=[]): TObject;
  43106. var ItemClass: TClass;
  43107. ItemInstance: TClassInstance;
  43108. begin
  43109. Valid := false;
  43110. result := nil;
  43111. if From=nil then
  43112. exit;
  43113. while From^ in [#1..' '] do inc(From);
  43114. if PInteger(From)^=NULL_LOW then begin
  43115. Valid := true;
  43116. exit;
  43117. end;
  43118. if From^<>'{' then
  43119. exit; // input should be either null, either {"ClassName":"TMyClass",...}
  43120. ItemClass := JSONSerializerRegisteredClass.Find(From,true);
  43121. if ItemClass=nil then
  43122. exit; // unknown type
  43123. ItemInstance.Init(ItemClass);
  43124. result := ItemInstance.CreateNew;
  43125. From := JSONToObject(result,From,Valid,nil,Options);
  43126. if not Valid then
  43127. FreeAndNil(result); // avoid memory leak
  43128. end;
  43129. type
  43130. TJSONObject =
  43131. (oNone, oException, oList, oObjectList, {$ifndef LVCL}oCollection,{$endif}
  43132. oUtfs, oStrings, oSQLRecord, oSQLMany, oPersistent, oPersistentPassword,
  43133. oSynMonitor, oSQLTable, oCustom);
  43134. function JSONObject(aClassType: TClass; out aCustomIndex: integer;
  43135. aExpectedReadWriteTypes: TJSONCustomParserExpectedDirections): TJSONObject;
  43136. const
  43137. MAX = {$ifdef LVCL}14{$else}15{$endif};
  43138. TYP: array[0..MAX] of TClass = ( // all classes types gathered in CPU L1 cache
  43139. TObject,Exception,ESynException,TList,TObjectList,TPersistent,
  43140. TSynPersistentWithPassword,TSynPersistent,TInterfacedObjectWithCustomCreate,
  43141. TSynMonitor,TSQLRecordMany,TSQLRecord,TStrings,TRawUTF8List,TSQLTable
  43142. {$ifndef LVCL},TCollection{$endif});
  43143. OBJ: array[0..MAX] of TJSONObject = (
  43144. oNone,oException,oPersistent,oList,oObjectList,oPersistent,
  43145. oPersistentPassword,oPersistent,oPersistent,
  43146. oSynMonitor,oSQLMany,oSQLRecord,oStrings,oUtfs,oSQLTable
  43147. {$ifndef LVCL},oCollection{$endif});
  43148. var i: integer;
  43149. begin
  43150. if aClassType<>nil then begin
  43151. aCustomIndex := JSONCustomParsersIndex(aClassType,aExpectedReadWriteTypes);
  43152. if aCustomIndex>=0 then begin
  43153. result := oCustom; // found exact custom type (ignore inherited)
  43154. exit;
  43155. end;
  43156. repeat // guess class type (faster than multiple InheritsFrom calls)
  43157. i := PtrUIntScanIndex(@TYP,MAX+1,PtrUInt(aClassType));
  43158. if i>=0 then begin
  43159. result := OBJ[i];
  43160. exit;
  43161. end;
  43162. {$ifdef FPC}
  43163. aClassType := aClassType.ClassParent;
  43164. {$else}
  43165. if PPointer(PtrInt(aClassType)+vmtParent)^<>nil then
  43166. aClassType := PPointer(PPointer(PtrInt(aClassType)+vmtParent)^)^ else
  43167. break;
  43168. {$endif}
  43169. until aClassType=nil;
  43170. end;
  43171. result := oNone;
  43172. end;
  43173. function PropIsIDTypeCastedField(Prop: PPropInfo; IsObj: TJSONObject;
  43174. Value: TObject): boolean; // see [22ce911c715]
  43175. begin
  43176. if (Value<>nil) and (Prop^.PropType^.ClassSQLFieldType=sftID) then
  43177. case IsObj of
  43178. oSQLMany:
  43179. if IdemPropName(Prop^.Name,'source') or IdemPropName(Prop^.Name,'dest') then
  43180. result := true else
  43181. result := not TSQLRecord(Value).fFill.JoinedFields;
  43182. oSQLRecord:
  43183. result := not TSQLRecord(Value).fFill.JoinedFields;
  43184. else result := false;
  43185. end else
  43186. result := false; // assume true instance by default
  43187. end;
  43188. function ObjectLoadJSON(var ObjectInstance; const JSON: RawUTF8;
  43189. TObjectListItemClass: TClass; Options: TJSONToObjectOptions): boolean;
  43190. var tmp: TSynTempBuffer;
  43191. begin
  43192. result := false;
  43193. tmp.Init(JSON);
  43194. if tmp.len<>0 then
  43195. try
  43196. JSONToObject(ObjectInstance,tmp.buf,result,TObjectListItemClass,Options);
  43197. finally
  43198. tmp.Done;
  43199. end;
  43200. end;
  43201. function JSONToObject(var ObjectInstance; From: PUTF8Char; var Valid: boolean;
  43202. TObjectListItemClass: TClass; Options: TJSONToObjectOptions): PUTF8Char;
  43203. var P: PPropInfo;
  43204. Value: TObject absolute ObjectInstance;
  43205. {$ifndef LVCL}
  43206. Coll: TCollection absolute ObjectInstance;
  43207. CollItem: TObject;
  43208. {$endif}
  43209. Str: TStrings absolute ObjectInstance;
  43210. Utf: TRawUTF8List absolute ObjectInstance;
  43211. Lst: TObjectList absolute ObjectInstance;
  43212. Item: TObject;
  43213. ItemInstance: TClassInstance;
  43214. ValueClass, ItemClass: TClass;
  43215. V: PtrInt;
  43216. err: integer;
  43217. E: TSynExtended;
  43218. V64: Int64;
  43219. PropName: PUTF8Char;
  43220. PropNameLen: integer;
  43221. PropValue: PUTF8Char;
  43222. EndOfObject: AnsiChar;
  43223. Kind: TTypeKind;
  43224. wasString, NestedValid: boolean;
  43225. IsObj: TJSONObject;
  43226. IsObjCustomIndex: integer;
  43227. s: string;
  43228. WS: WideString;
  43229. U: RawUTF8;
  43230. {$ifndef NOVARIANTS}
  43231. VVariant: variant;
  43232. DocVariantOptionsSet: TDocVariantOptions;
  43233. label doProp;
  43234. {$endif}
  43235. begin
  43236. Valid := false;
  43237. result := From;
  43238. if Value=nil then
  43239. exit;
  43240. ValueClass := Value.ClassType;
  43241. IsObj := JSONObject(ValueClass,IsObjCustomIndex,[cpRead]);
  43242. if From=nil then begin
  43243. case IsObj of // handle '' as Clear for arrays
  43244. {$ifndef LVCL}
  43245. oCollection: Coll.Clear;
  43246. {$endif}
  43247. oStrings: Str.Clear;
  43248. oUtfs: Utf.Clear;
  43249. oObjectList: Lst.Clear;
  43250. end;
  43251. exit;
  43252. end;
  43253. if PInteger(From)^=NULL_LOW then begin
  43254. if (IsObj=oCustom) and Assigned(JSONCustomParsers[IsObjCustomIndex].Reader) then
  43255. // custom JSON reader expects to be executed even if value is null
  43256. result := JSONCustomParsers[IsObjCustomIndex].Reader(Value,From,Valid,Options) else begin
  43257. FreeAndNil(Value);
  43258. result := From+4;
  43259. Valid := true; // null is a valid JSON object
  43260. end;
  43261. exit;
  43262. end;
  43263. while From^ in [#1..' '] do inc(From);
  43264. if IsObj=oCustom then
  43265. with JSONCustomParsers[IsObjCustomIndex] do begin
  43266. if Assigned(Reader) then // leave Valid=false if Reader=nil
  43267. result := Reader(Value,From,Valid,Options);
  43268. exit;
  43269. end;
  43270. if From^='[' then begin
  43271. // nested array = TObjectList, TCollection, TRawUTF8List or TStrings
  43272. inc(From);
  43273. case IsObj of
  43274. oObjectList: begin // TList leaks memory, but TObjectList uses "ClassName":..
  43275. Lst.Clear;
  43276. ItemInstance.ItemClass := nil;
  43277. repeat
  43278. while From^ in [#1..' '] do inc(From);
  43279. case From^ of
  43280. #0: exit;
  43281. ']': begin
  43282. inc(From);
  43283. break;
  43284. end;
  43285. ',':
  43286. inc(From); // valid delimiter between objects
  43287. '{': begin
  43288. result := From;
  43289. if TObjectListItemClass=nil then begin // recognize "ClassName":...
  43290. ItemClass := JSONSerializerRegisteredClass.Find(From,true);
  43291. if ItemClass=nil then
  43292. exit; // unknown "ClassName":.. type
  43293. end else
  43294. ItemClass := TObjectListItemClass;
  43295. if ItemInstance.ItemClass<>ItemClass then
  43296. ItemInstance.Init(ItemClass);
  43297. Item := ItemInstance.CreateNew;
  43298. From := JSONToObject(Item,From,NestedValid,nil,Options);
  43299. if not NestedValid then begin
  43300. result := From;
  43301. exit;
  43302. end else
  43303. if From=nil then
  43304. exit;
  43305. Lst.Add(Item);
  43306. end;
  43307. else exit;
  43308. end;
  43309. until false;
  43310. // only way of being here is to have an ending ] at expected place
  43311. Valid := true;
  43312. end;
  43313. {$ifndef LVCL}
  43314. oCollection: begin
  43315. Coll.BeginUpdate; // Coll: TCollection absolute Value
  43316. try
  43317. Coll.Clear;
  43318. repeat
  43319. while From^ in [#1..' '] do inc(From);
  43320. case From^ of
  43321. #0: exit;
  43322. ']': begin
  43323. inc(From);
  43324. break;
  43325. end;
  43326. ',':
  43327. inc(From); // valid delimiter between objects
  43328. '{': begin
  43329. result := From;
  43330. CollItem := Coll.Add;
  43331. From := JSONToObject(CollItem,From,NestedValid,nil,Options);
  43332. if not NestedValid then begin
  43333. result := From;
  43334. exit;
  43335. end else
  43336. if From=nil then
  43337. exit;
  43338. end;
  43339. else exit;
  43340. end;
  43341. until false;
  43342. // only way of being here is to have an ending ] at expected place
  43343. Valid := true;
  43344. finally
  43345. Coll.EndUpdate;
  43346. end;
  43347. end;
  43348. {$endif}
  43349. oStrings: begin
  43350. {$ifndef LVCL}
  43351. Str.BeginUpdate; // Str: TStrings absolute Value
  43352. try
  43353. {$endif}
  43354. Str.Clear;
  43355. repeat
  43356. while From^ in [#1..' '] do inc(From);
  43357. case From^ of
  43358. #0: exit;
  43359. ']': begin
  43360. inc(From);
  43361. break;
  43362. end;
  43363. '"': begin
  43364. result := From;
  43365. PropValue := GetJSONField(From,From,@wasString,@EndOfObject);
  43366. if (PropValue=nil) or not wasString then
  43367. exit;
  43368. UTF8DecodeToString(PropValue,StrLen(PropValue),s);
  43369. Str.Add(s);
  43370. case EndOfObject of
  43371. ']': break;
  43372. ',': continue;
  43373. else exit;
  43374. end;
  43375. end;
  43376. else exit;
  43377. end;
  43378. until false;
  43379. Valid := true;
  43380. {$ifndef LVCL}
  43381. finally
  43382. Str.EndUpdate;
  43383. end;
  43384. {$endif}
  43385. end;
  43386. oUtfs: begin
  43387. utf.BeginUpdate; // utf: TRawUTF8List absolute Value
  43388. try
  43389. utf.Clear;
  43390. repeat
  43391. while From^ in [#1..' '] do inc(From);
  43392. case From^ of
  43393. #0: exit;
  43394. ']': begin
  43395. inc(From);
  43396. break;
  43397. end;
  43398. '"': begin
  43399. result := From;
  43400. PropValue := GetJSONField(From,From,@wasString,@EndOfObject);
  43401. if (PropValue=nil) or not wasString then
  43402. exit;
  43403. SetString(U,PAnsiChar(PropValue),StrLen(PropValue));
  43404. utf.Add(U);
  43405. case EndOfObject of
  43406. ']': break;
  43407. ',': if From=nil then exit else continue;
  43408. else exit;
  43409. end;
  43410. end;
  43411. else exit;
  43412. end;
  43413. until false;
  43414. Valid := true;
  43415. finally
  43416. utf.EndUpdate;
  43417. end;
  43418. end;
  43419. end; // case IsObj of
  43420. // Valid=false if not TCollection, TRawUTF8List nor TStrings
  43421. if Valid and (From<>nil) then begin
  43422. while From^ in [#1..' '] do inc(From);
  43423. if From^=#0 then
  43424. From := nil;
  43425. end;
  43426. result := From;
  43427. exit; // a JSON array begin with [
  43428. end else
  43429. if From^<>'{' then begin
  43430. result := From;
  43431. exit; // a JSON object MUST begin with {
  43432. end;
  43433. repeat inc(From) until (From^=#0) or (From^>' ');
  43434. EndOfObject := #0;
  43435. if From^='}' then begin
  43436. // empty JSON object like {} (e.g. all properties having default values)
  43437. EndOfObject := '}';
  43438. Inc(From);
  43439. end else
  43440. repeat
  43441. wasString := false;
  43442. result := From;
  43443. PropName := GetJSONPropName(From); // get property name
  43444. PropNameLen := StrLen(PropName);
  43445. if (From=nil) or (PropNameLen=0) then
  43446. exit; // invalid JSON content
  43447. if IdemPropName('ClassName',PropName,PropNameLen) then begin
  43448. // WriteObject() was called with woStoreClassName option -> handle it
  43449. PropValue := GetJSONField(From,From,@wasString,@EndOfObject);
  43450. if (PropValue=nil) or (not wasString) or not (EndOfObject in ['}',',']) then
  43451. exit; // invalid JSON content
  43452. continue; // just ignore the field here
  43453. end;
  43454. if (IsObj in [oSQLRecord,oSQLMany]) and IsRowID(PropName) then begin
  43455. // manual handling of TSQLRecord.ID property unserialization
  43456. PropValue := GetJSONField(From,From,@wasString,@EndOfObject);
  43457. if (PropValue=nil) or wasString or not (EndOfObject in ['}',',']) then
  43458. exit; // invalid JSON content
  43459. SetID(PropValue,TSQLRecord(Value).fID);
  43460. continue;
  43461. end;
  43462. P := ClassFieldPropWithParentsFromUTF8(ValueClass,PropName,PropNameLen);
  43463. if P=nil then // unknown property
  43464. if j2oIgnoreUnknownProperty in Options then begin
  43465. From := GotoNextJSONItem(From,1,@EndOfObject);
  43466. continue;
  43467. end else
  43468. exit; // by default, abort
  43469. Kind := P^.PropType^.Kind;
  43470. while From^ in [#1..' '] do inc(From);
  43471. result := From;
  43472. if PInteger(From)^=NULL_LOW then begin
  43473. // null value should set the default value, or free nested object
  43474. if (Kind=tkClass) and (IsObj in [oSQLRecord,oSQLMany]) then
  43475. exit; // null expects a plain TSynPersistent/TPersistent
  43476. P^.SetDefaultValue(Value); // will set 0,'' or FreeAndNil(NestedObject)
  43477. inc(From,4);
  43478. while From^ in [#1..' '] do inc(From);
  43479. EndOfObject := From^;
  43480. if From^ in EndOfJSONField then
  43481. inc(From);
  43482. end else
  43483. if From^ in ['[','{'] then begin
  43484. // nested array or object
  43485. if Kind=tkDynArray then begin
  43486. From := P^.GetDynArray(Value).LoadFromJSON(From);
  43487. if From=nil then
  43488. exit; // invalid '[dynamic array]' content
  43489. end else
  43490. {$ifndef NOVARIANTS}
  43491. if Kind=tkVariant then
  43492. goto doProp else
  43493. {$endif}
  43494. if (Kind=tkSet) and (From^='[') then begin // set as string array
  43495. V := GetSetNameValue(P^.TypeInfo,From,EndOfObject);
  43496. P^.SetOrdProp(Value,V);
  43497. end else
  43498. if (Kind in tkRecordTypes) and (From^='{') then begin // from Delphi XE5+
  43499. From := RecordLoadJSON(P^.GetFieldAddr(Value)^,From,P^.TypeInfo,@EndOfObject);
  43500. if From=nil then
  43501. exit; // invalid '{record}' content
  43502. if EndOfObject='}' then
  43503. break else
  43504. continue;
  43505. end else begin
  43506. if Kind<>tkClass then
  43507. exit; // true nested object should begin with '[' or '{'
  43508. if PropIsIDTypeCastedField(P,IsObj,Value) then
  43509. exit; // only TSQLRecordMany/joined properties are true instances
  43510. // will handle '[TCollection...' '[TStrings...' '{TObject...'
  43511. From := P^.ClassFromJSON(Value,From,NestedValid,Options);
  43512. if not NestedValid then begin
  43513. result := From;
  43514. exit;
  43515. end else
  43516. if From=nil then
  43517. exit; // invalid JSON content: we expect at least a last '}'
  43518. end;
  43519. while From^ in [#1..' '] do inc(From);
  43520. EndOfObject := From^;
  43521. if From^ in EndOfJSONField then
  43522. inc(From);
  43523. end else begin
  43524. doProp: // normal property value
  43525. PropValue := GetJSONFieldOrObjectOrArray(From,@wasString,@EndOfObject
  43526. {$ifndef NOVARIANTS},Kind=tkVariant{$endif});
  43527. if (PropValue=nil) or not (EndOfObject in ['}',',']) then
  43528. exit; // invalid JSON content (null has been handled above)
  43529. case Kind of
  43530. tkInt64{$ifdef FPC}, tkQWord{$endif}:
  43531. if wasString then
  43532. exit else begin
  43533. V64 := GetInt64(PropValue,err);
  43534. if err<>0 then
  43535. exit;
  43536. P^.SetInt64Prop(Value,V64);
  43537. end;
  43538. tkClass: begin
  43539. if wasString or (P^.PropType^.ClassSQLFieldType<>sftID) then
  43540. exit; // should have been handled above
  43541. V := GetInteger(PropValue,err);
  43542. if err<>0 then
  43543. exit; // invalid value
  43544. P^.SetOrdProp(Value,V);
  43545. end;
  43546. tkEnumeration: begin
  43547. if wasString then begin // in case enum stored as string
  43548. V := P^.PropType^.EnumBaseType^.GetEnumNameValue(PropValue);
  43549. if V<0 then
  43550. if j2oIgnoreUnknownEnum in Options then
  43551. V := 0 else
  43552. exit;
  43553. end else begin
  43554. V := GetInteger(PropValue,err);
  43555. if err<>0 then
  43556. if j2oIgnoreUnknownEnum in Options then
  43557. V := 0 else
  43558. exit; // invalid value
  43559. end;
  43560. P^.SetOrdProp(Value,V);
  43561. end;
  43562. {$ifdef FPC} tkBool, {$endif}
  43563. tkInteger, tkSet:
  43564. if wasString then
  43565. exit else begin
  43566. // From='true' or From='false' were converted into '1 or '0'
  43567. V := GetInteger(PropValue,err);
  43568. if err<>0 then
  43569. exit; // invalid value
  43570. P^.SetOrdProp(Value,V);
  43571. end;
  43572. {$ifdef FPC}tkAString,{$endif} tkLString:
  43573. if wasString or (j2oIgnoreStringType in Options) then begin
  43574. SetString(U,PAnsiChar(PropValue),StrLen(PropValue));
  43575. P^.SetLongStrValue(Value,U);
  43576. end else
  43577. exit;
  43578. {$ifdef HASVARUSTRING}
  43579. tkUString:
  43580. if wasString or (j2oIgnoreStringType in Options) then
  43581. P^.SetUnicodeStrProp(Value,
  43582. UTF8DecodeToUnicodeString(PropValue,StrLen(PropValue))) else
  43583. exit;
  43584. {$endif}
  43585. tkWString:
  43586. if wasString or (j2oIgnoreStringType in Options) then begin
  43587. UTF8ToWideString(PropValue,StrLen(PropValue),WS);
  43588. P^.SetWideStrProp(Value,WS);
  43589. end else
  43590. exit;
  43591. {$ifdef PUBLISHRECORD}
  43592. tkRecord{$ifdef FPC},tkObject{$endif}:
  43593. if not wasString then
  43594. exit else
  43595. RecordLoadJSON(P^.GetFieldAddr(Value)^,PropValue,P^.TypeInfo);
  43596. {$endif}
  43597. {$ifndef NOVARIANTS}
  43598. tkVariant: begin
  43599. if j2oHandleCustomVariants in Options then begin
  43600. if j2oHandleCustomVariantsWithinString in Options then
  43601. DocVariantOptionsSet := [dvoValueCopiedByReference,dvoJSONObjectParseWithinString] else
  43602. DocVariantOptionsSet := [dvoValueCopiedByReference];
  43603. GetVariantFromJSON(PropValue,wasString,VVariant,@DocVariantOptionsSet);
  43604. end else
  43605. GetVariantFromJSON(PropValue,wasString,VVariant);
  43606. P^.SetVariantProp(Value,VVariant);
  43607. end;
  43608. {$endif}
  43609. tkFloat:
  43610. if P^.TypeInfo=TypeInfo(TDateTime) then
  43611. if wasString then begin
  43612. if PInteger(PropValue)^ and $ffffff=JSON_SQLDATE_MAGIC then
  43613. inc(PropValue,3); // ignore U+FFF1 pattern
  43614. P^.SetFloatProp(Value,Iso8601ToDateTimePUTF8Char(PropValue,0));
  43615. end else
  43616. exit else
  43617. if wasString then
  43618. exit else
  43619. if (P^.TypeInfo=TypeInfo(Currency)) and P^.SetterIsField then
  43620. PInt64(P^.SetterAddr(Value))^ := StrToCurr64(PropValue) else begin
  43621. E := GetExtended(pointer(PropValue),err);
  43622. if err<>0 then
  43623. exit else // invalid JSON content
  43624. P^.SetFloatProp(Value,E);
  43625. end;
  43626. end;
  43627. end;
  43628. until (From=nil) or (EndOfObject='}');
  43629. if From<>nil then begin
  43630. while From^ in [#1..' '] do inc(From);
  43631. if From^=#0 then
  43632. From := nil;
  43633. end;
  43634. Valid := (EndOfObject='}'); // mark parsing success
  43635. result := From;
  43636. end;
  43637. function UrlDecodeObject(U: PUTF8Char; Upper: PAnsiChar; var ObjectInstance;
  43638. Next: PPUTF8Char=nil; Options: TJSONToObjectOptions=[]): boolean;
  43639. var tmp: RawUTF8;
  43640. begin
  43641. result := UrlDecodeValue(U, Upper, tmp, Next);
  43642. if result then
  43643. JSONToObject(ObjectInstance,Pointer(tmp),result,nil,Options);
  43644. end;
  43645. function JSONFileToObject(const JSONFile: TFileName; var ObjectInstance;
  43646. TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean;
  43647. var tmp: RawUTF8;
  43648. begin
  43649. tmp := AnyTextFileToRawUTF8(JSONFile,true);
  43650. if tmp='' then
  43651. result := false else begin
  43652. RemoveCommentsFromJSON(pointer(tmp));
  43653. JSONToObject(ObjectInstance,pointer(tmp),result,TObjectListItemClass,Options);
  43654. end;
  43655. end;
  43656. function ObjectToJSONFile(Value: TObject; const JSONFile: TFileName;
  43657. Options: TTextWriterWriteObjectOptions): boolean;
  43658. var humanread: boolean;
  43659. json: RawUTF8;
  43660. begin
  43661. humanread := woHumanReadable in Options;
  43662. Exclude(Options,woHumanReadable);
  43663. json := ObjectToJSON(Value,Options);
  43664. if humanread then
  43665. // woHumanReadable not working with custom JSON serializers, e.g. T*ObjArray
  43666. result := JSONBufferReformatToFile(pointer(json),JSONFile) else
  43667. result := FileFromString(json,JSONFile);
  43668. end;
  43669. procedure ReadObject(Value: TObject; From: PUTF8Char; const SubCompName: RawUTF8=''); overload;
  43670. var P: PPropInfo;
  43671. i, V, err: integer;
  43672. V64: Int64;
  43673. E: TSynExtended;
  43674. Obj: TObject;
  43675. UpperName: array[byte] of AnsiChar;
  43676. U: RawUTF8;
  43677. {$ifndef NOVARIANTS}
  43678. VVariant: variant;
  43679. {$endif}
  43680. begin
  43681. if Value=nil then // allow From=nil -> default values
  43682. exit;
  43683. for i := 1 to InternalClassPropInfo(Value.ClassType,P) do begin
  43684. PWord(UpperCopyShort(UpperCopy255(UpperName,SubCompName),P^.Name))^ := ord('=');
  43685. U := FindIniNameValue(From,UpperName);
  43686. case P^.PropType^.Kind of
  43687. tkInt64{$ifdef FPC}, tkQWord{$endif}: begin
  43688. V64 := GetInt64(pointer(U),err);
  43689. if err=0 then
  43690. P^.SetInt64Prop(Value,V64); // pointer() to call typinfo
  43691. end;
  43692. {$ifdef FPC}tkBool,{$endif} tkEnumeration, tkSet, tkInteger: begin
  43693. V := GetInteger(pointer(U),err);
  43694. if err=0 then
  43695. P^.SetOrdProp(Value,V) else // pointer() to call typinfo
  43696. if P^.Default<>longint($80000000) then
  43697. P^.SetOrdProp(Value,P^.Default);
  43698. end;
  43699. tkFloat:
  43700. if U<>'' then
  43701. if (P^.TypeInfo=TypeInfo(Currency)) and P^.SetterIsField then
  43702. PInt64(P^.SetterAddr(Value))^ := StrToCurr64(pointer(U)) else begin
  43703. E := GetExtended(pointer(U),err);
  43704. if err=0 then
  43705. P^.SetFloatProp(Value,E);
  43706. end;
  43707. {$ifdef FPC}tkAString,{$endif} tkLString:
  43708. P^.SetLongStrValue(Value,U);
  43709. tkWString:
  43710. P^.SetWideStrProp(Value,UTF8ToWideString(U));
  43711. {$ifdef HASVARUSTRING}
  43712. tkUString:
  43713. P^.SetUnicodeStrProp(Value,UTF8ToString(U));
  43714. {$endif}
  43715. tkDynArray:
  43716. P^.GetDynArray(Value).LoadFrom(pointer(BlobToTSQLRawBlob(U)));
  43717. {$ifdef PUBLISHRECORD}
  43718. tkRecord{$ifdef FPC},tkObject{$endif}:
  43719. RecordLoadJSON(P^.GetFieldAddr(Value)^,pointer(U),P^.PropType^);
  43720. {$endif PUBLISHRECORD}
  43721. tkClass: begin
  43722. Obj := P^.GetObjProp(Value);
  43723. if {$ifdef MSWINDOWS}(PtrUInt(Obj)>=PtrUInt(SystemInfo.lpMinimumApplicationAddress)) and{$endif}
  43724. Obj.InheritsFrom(TPersistent) then
  43725. ReadObject(Obj,From,SubCompName+ToUTF8(P^.Name)+'.');
  43726. end;
  43727. {$ifndef NOVARIANTS}
  43728. tkVariant: begin
  43729. VariantLoadJSON(VVariant,pointer(U));
  43730. P^.SetVariantProp(Value,VVariant);
  43731. end;
  43732. {$endif} // tkString (shortstring) and tkInterface is not handled
  43733. end;
  43734. P := P^.Next;
  43735. end;
  43736. end;
  43737. procedure ReadObject(Value: TObject; const FromContent: RawUTF8;
  43738. const SubCompName: RawUTF8=''); overload;
  43739. var source: PUTF8Char;
  43740. UpperSection: array[byte] of AnsiChar;
  43741. begin
  43742. if Value=nil then
  43743. exit; // avoid GPF
  43744. PWord(UpperCopyShort(UpperSection,PShortString(PPointer(
  43745. PPtrInt(Value)^+vmtClassName)^)^))^ := ord(']');
  43746. source := pointer(FromContent);
  43747. if FindSectionFirstLine(source,UpperSection) then
  43748. ReadObject(Value,source,SubCompName);
  43749. end;
  43750. procedure SetDefaultValuesObject(Value: TObject);
  43751. var p: PPropInfo;
  43752. c: TClass;
  43753. i: integer;
  43754. begin
  43755. if Value=nil then
  43756. exit;
  43757. c := Value.ClassType;
  43758. repeat
  43759. for i := 1 to InternalClassPropInfo(Value.ClassType,p) do begin
  43760. case p^.PropType^.Kind of
  43761. {$ifdef FPC}tkBool,{$endif} tkEnumeration, tkSet, tkInteger:
  43762. if p^.Default<>longint($80000000) then
  43763. p^.SetOrdProp(Value,p^.Default);
  43764. tkClass:
  43765. SetDefaultValuesObject(p^.GetObjProp(Value));
  43766. end;
  43767. p := p^.Next;
  43768. end;
  43769. c := c.ClassParent;
  43770. until c=nil;
  43771. end;
  43772. procedure ClearObject(Value: TObject; FreeAndNilNestedObjects: boolean=false);
  43773. var p: PPropInfo;
  43774. c: TClass;
  43775. i: integer;
  43776. begin
  43777. if Value=nil then
  43778. exit;
  43779. c := Value.ClassType;
  43780. repeat
  43781. for i := 1 to InternalClassPropInfo(c,p) do begin
  43782. p^.SetDefaultValue(Value,FreeAndNilNestedObjects);
  43783. {$ifdef HASINLINE}
  43784. p := p^.Next;
  43785. {$else}
  43786. with p^ do p := @Name[ord(Name[0])+1];
  43787. {$endif}
  43788. end;
  43789. c := c.ClassParent;
  43790. until c=nil;
  43791. end;
  43792. { TClassInstance }
  43793. procedure TClassInstance.Init(C: TClass);
  43794. begin
  43795. ItemClass := C;
  43796. if C<>nil then
  43797. repeat // this unrolled loop is faster than cascaded if C.InheritsFrom()
  43798. if C<>TSQLRecord then
  43799. if C<>TObjectList then
  43800. if C<>TInterfacedObjectWithCustomCreate then
  43801. if C<>TPersistentWithCustomCreate then
  43802. if C<>TSynPersistent then
  43803. if C<>TComponent then
  43804. {$ifndef LVCL}
  43805. if C<>TInterfacedCollection then
  43806. if C<>TCollection then
  43807. if C<>TCollectionItem then
  43808. {$endif}
  43809. {$ifdef FPC}
  43810. if C.ClassParent<>nil then begin
  43811. C := C.ClassParent;
  43812. {$else}
  43813. if PPointer(PtrInt(C)+vmtParent)^<>nil then begin
  43814. C := PPointer(PPointer(PtrInt(C)+vmtParent)^)^;
  43815. {$endif}
  43816. if C<>nil then
  43817. continue else begin
  43818. ItemCreate := cicTObject;
  43819. exit;
  43820. end;
  43821. end else begin
  43822. ItemCreate := cicTObject;
  43823. exit;
  43824. end else
  43825. {$ifndef LVCL} begin
  43826. ItemCreate := cicTCollectionItem;
  43827. exit;
  43828. end else begin // plain TCollection shall have been registered
  43829. CollectionItemClass := JSONSerializerRegisteredCollection.Find(TCollectionClass(ItemClass));
  43830. if CollectionItemClass<>nil then begin
  43831. ItemCreate := cicTCollection;
  43832. exit;
  43833. end else
  43834. raise EParsingException.CreateUTF8('% shall inherit from TInterfacedCollection'+
  43835. ' or call TJSONSerializer.RegisterCollectionForJSON()',[ItemClass]);
  43836. end else begin
  43837. ItemCreate := cicTInterfacedCollection;
  43838. exit;
  43839. end else
  43840. {$endif} begin
  43841. ItemCreate := cicTComponent;
  43842. exit;
  43843. end else begin
  43844. ItemCreate := cicTSynPersistent;
  43845. exit;
  43846. end else begin
  43847. ItemCreate := cicTPersistentWithCustomCreate;
  43848. exit;
  43849. end else begin
  43850. ItemCreate := cicTInterfacedObjectWithCustomCreate;
  43851. exit;
  43852. end else begin
  43853. ItemCreate := cicTObjectList;
  43854. exit;
  43855. end else begin
  43856. ItemCreate := cicTSQLRecord;
  43857. exit;
  43858. end;
  43859. until false;
  43860. ItemCreate := cicUnknown;
  43861. end;
  43862. function TClassInstance.CreateNew: TObject;
  43863. begin
  43864. if @self<>nil then
  43865. case ItemCreate of
  43866. cicUnknown: begin
  43867. result := nil;
  43868. exit;
  43869. end;
  43870. cicTSQLRecord: begin
  43871. result := TSQLRecordClass(ItemClass).Create;
  43872. exit;
  43873. end;
  43874. cicTObjectList: begin
  43875. result := TObjectList.Create;
  43876. exit;
  43877. end;
  43878. cicTPersistentWithCustomCreate: begin
  43879. result := TPersistentWithCustomCreateClass(ItemClass).Create;
  43880. exit;
  43881. end;
  43882. cicTComponent: begin
  43883. result := TComponentClass(ItemClass).Create(nil);
  43884. exit;
  43885. end;
  43886. cicTSynPersistent: begin
  43887. result := TSynPersistentClass(ItemClass).Create;
  43888. exit;
  43889. end;
  43890. cicTInterfacedObjectWithCustomCreate: begin
  43891. result := TInterfacedObjectWithCustomCreateClass(ItemClass).Create;
  43892. exit;
  43893. end;
  43894. {$ifndef LVCL}
  43895. cicTInterfacedCollection: begin
  43896. result := TInterfacedCollectionClass(ItemClass).Create;
  43897. exit;
  43898. end;
  43899. cicTCollection: begin
  43900. result := TCollectionClass(ItemClass).Create(CollectionItemClass);
  43901. exit;
  43902. end;
  43903. cicTCollectionItem: begin
  43904. result := TCollectionItemClass(ItemClass).Create(nil);
  43905. exit;
  43906. end;
  43907. {$endif}
  43908. cicTObject: begin
  43909. result := ItemClass.Create;
  43910. exit;
  43911. end;
  43912. else begin
  43913. result := nil;
  43914. exit;
  43915. end;
  43916. end else begin
  43917. result := nil;
  43918. exit;
  43919. end;
  43920. end;
  43921. {$ifdef MSWINDOWS}
  43922. { TSQLRestClientURIMessage }
  43923. constructor TSQLRestClientURIMessage.Create(aModel: TSQLModel;
  43924. const ServerWindowName: string; ClientWindow: HWND; TimeOutMS: cardinal);
  43925. begin
  43926. inherited Create(aModel);
  43927. fClientWindow := ClientWindow;
  43928. fServerWindowName := ServerWindowName;
  43929. fTimeOutMS := TimeOutMS;
  43930. end;
  43931. constructor TSQLRestClientURIMessage.Create(aModel: TSQLModel;
  43932. const ServerWindowName, ClientWindowName: string; TimeOutMS: cardinal);
  43933. var H: HWND;
  43934. begin
  43935. H := CreateInternalWindow(ClientWindowName,self);
  43936. if H=0 then
  43937. raise ECommunicationException.CreateUTF8('%.Create(): CreateInternalWindow("%")',
  43938. [self,ClientWindowName]);
  43939. fClientWindowName := ClientWindowName;
  43940. Create(aModel,ServerWindowName,H,TimeOutMS);
  43941. end;
  43942. destructor TSQLRestClientURIMessage.Destroy;
  43943. begin
  43944. try
  43945. inherited Destroy;
  43946. finally
  43947. ReleaseInternalWindow(fClientWindowName,fClientWindow);
  43948. end;
  43949. end;
  43950. procedure TSQLRestClientURIMessage.DefinitionTo(Definition: TSynConnectionDefinition);
  43951. begin
  43952. if Definition=nil then
  43953. exit;
  43954. inherited DefinitionTo(Definition); // save Kind + User/Password
  43955. Definition.ServerName := StringToUTF8(fServerWindowName);
  43956. Definition.DatabaseName := StringToUTF8(fClientWindowName);
  43957. end;
  43958. constructor TSQLRestClientURIMessage.RegisteredClassCreateFrom(aModel: TSQLModel;
  43959. aDefinition: TSynConnectionDefinition);
  43960. begin
  43961. Create(aModel,UTF8ToString(aDefinition.ServerName),
  43962. UTF8ToString(aDefinition.DatabaseName),10000);
  43963. inherited RegisteredClassCreateFrom(aModel,aDefinition); // call SetUser()
  43964. end;
  43965. procedure TSQLRestClientURIMessage.InternalURI(var Call: TSQLRestURIParams);
  43966. var Msg: RawUTF8;
  43967. Data: TCopyDataStruct;
  43968. Finished64: Int64;
  43969. P: PUTF8Char;
  43970. aMsg: TMsg;
  43971. {$ifdef WITHLOG}
  43972. Log: ISynLog;
  43973. {$endif}
  43974. begin
  43975. {$ifdef WITHLOG}
  43976. Log := fLogClass.Enter(self);
  43977. {$endif}
  43978. if (fClientWindow=0) or not InternalCheckOpen then begin
  43979. Call.OutStatus := HTML_NOTIMPLEMENTED; // 501
  43980. InternalLog('InternalCheckOpen failure',sllClient);
  43981. exit;
  43982. end;
  43983. // 1. send request
  43984. // #1 is a field delimiter below, since Get*Item() functions return nil for #0
  43985. SetString(Msg,PAnsiChar(@MAGIC_SYN),4);
  43986. Msg := Msg+Call.Url+#1+Call.Method+#1+Call.InHead+#1+Call.InBody;
  43987. Data.dwData := fClientWindow;
  43988. Data.cbData := length(Msg)*SizeOf(Msg[1]);
  43989. Data.lpData := pointer(Msg);
  43990. fSafe.Enter;
  43991. try
  43992. fCurrentResponse := #0; // mark expect some response
  43993. Call.OutStatus := SendMessage(fServerWindow,WM_COPYDATA,fClientWindow,PtrInt(@Data));
  43994. if not StatusCodeIsSuccess(Call.OutStatus) then begin
  43995. fCurrentResponse := '';
  43996. with Call do
  43997. InternalLog('% % status=%',[Method,Url,OutStatus],sllError);
  43998. exit;
  43999. end;
  44000. // 2. expect answer from server
  44001. if fCurrentResponse=#0 then begin
  44002. // in practice, we never reach here since SendMessage() did wait for the
  44003. // message to be processed by the receiver, so the Server should have
  44004. // already answered and fCurrentResponse field should have been set
  44005. Finished64 := GetTickCount64+fTimeOutMS;
  44006. repeat
  44007. // incoming WM_COPYDATA will set fCurrentResponse in WMCopyData() method
  44008. if not DoNotProcessMessages then
  44009. while PeekMessage(aMsg,0,0,0,PM_REMOVE) do begin
  44010. TranslateMessage(aMsg);
  44011. DispatchMessage(aMsg);
  44012. end;
  44013. SleepHiRes(0);
  44014. if GetTickCount64>Finished64 then begin
  44015. Call.OutStatus := HTML_TIMEOUT; // 408 Request Timeout Error
  44016. exit;
  44017. end;
  44018. until fCurrentResponse<>#0;
  44019. end;
  44020. // 3. return answer to caller
  44021. if length(fCurrentResponse)<=sizeof(Int64) then
  44022. Call.OutStatus := HTML_NOTIMPLEMENTED else begin
  44023. P := pointer(fCurrentResponse);
  44024. if PCardinal(P)^<>MAGIC_SYN then // broadcasted WM_COPYDATA message? :(
  44025. Call.OutStatus := 0 else begin
  44026. Call.OutStatus := PIntegerArray(P)[1];
  44027. Call.OutInternalState := PIntegerArray(P)[2];
  44028. inc(P,sizeof(integer)*3);
  44029. end;
  44030. if Call.OutStatus=0 then
  44031. Call.OutStatus := HTML_NOTFOUND else begin
  44032. Call.OutHead := GetNextItem(P,#1);
  44033. if P<>nil then
  44034. SetString(Call.OutBody,P,length(fCurrentResponse)-(P-pointer(fCurrentResponse)));
  44035. end;
  44036. end;
  44037. finally
  44038. fSafe.Leave;
  44039. end;
  44040. with Call do
  44041. InternalLog('% % status=% state=%',[Method,Url,OutStatus,OutInternalState],sllClient);
  44042. end;
  44043. procedure TSQLRestClientURIMessage.WMCopyData(var Msg: TWMCopyData);
  44044. begin
  44045. if (self=nil) or (Msg.From<>fServerWindow) or
  44046. (PCopyDataStruct(Msg.CopyDataStruct)^.dwData<>fServerWindow) then
  44047. exit;
  44048. Msg.Result := HTML_SUCCESS; // Send something back
  44049. if fCurrentResponse=#0 then // expect some response?
  44050. SetString(fCurrentResponse,PAnsiChar(PCopyDataStruct(Msg.CopyDataStruct)^.lpData),
  44051. PCopyDataStruct(Msg.CopyDataStruct)^.cbData);
  44052. end;
  44053. function TSQLRestClientURIMessage.InternalCheckOpen: boolean;
  44054. begin
  44055. fSafe.Enter;
  44056. try
  44057. if fServerWindow<>0 then begin
  44058. result := true;
  44059. exit; // only reconnect if forced by InternalClose call or at first access
  44060. end;
  44061. fServerWindow := FindWindow(pointer(fServerWindowName),nil);
  44062. result := fServerWindow<>0;
  44063. finally
  44064. fSafe.Leave;
  44065. end;
  44066. end;
  44067. procedure TSQLRestClientURIMessage.InternalClose;
  44068. begin
  44069. fServerWindow := 0;
  44070. end;
  44071. {$endif}
  44072. { TSQLRecordSigned }
  44073. function TSQLRecordSigned.CheckSignature(const Content: RawByteString): boolean;
  44074. var tmp: RawUTF8;
  44075. i: integer;
  44076. SHA: TSHA256;
  44077. Digest: TSHA256Digest;
  44078. begin
  44079. result := false;
  44080. i := PosEx(RawUTF8('/'),fSignature,1);
  44081. if i=0 then
  44082. exit;
  44083. tmp := TTimeLogBits(fSignatureTime).Text(false)+RawUTF8(ClassName)+copy(fSignature,1,i-1);
  44084. SHA.Init;
  44085. SHA.Update(pointer(tmp),length(tmp));
  44086. SHA.Update(pointer(Content),length(Content)); // hash in place: no Content copy
  44087. SHA.Final(Digest);
  44088. if SHA256DigestToString(Digest)=copy(fSignature,i+1,sizeof(Digest)*2) then
  44089. result := true;
  44090. end;
  44091. function TSQLRecordSigned.SetAndSignContent(const UserName: RawUTF8;
  44092. const Content: RawByteString; ForcedSignatureTime: Int64): boolean;
  44093. var tmp: RawUTF8;
  44094. SHA: TSHA256;
  44095. Digest: TSHA256Digest;
  44096. begin
  44097. result := (fSignature='') and (fSignatureTime=0);
  44098. if not result then
  44099. exit; // sign is allowed only once
  44100. if ForcedSignatureTime<>0 then
  44101. fSignatureTime := ForcedSignatureTime else
  44102. fSignatureTime := TimeLogNow;
  44103. { content is hashed with User Name value }
  44104. tmp := TTimeLogBits(fSignatureTime).Text(false)+RawUTF8(ClassName)+UserName;
  44105. SHA.Init;
  44106. SHA.Update(pointer(tmp),length(tmp));
  44107. SHA.Update(pointer(Content),length(Content)); // hash in place: no Content copy
  44108. SHA.Final(Digest);
  44109. fSignature := UserName+'/'+SHA256DigestToString(Digest);
  44110. end;
  44111. function TSQLRecordSigned.SignedBy: RawUTF8;
  44112. var i: integer;
  44113. begin
  44114. i := PosEx(RawUTF8('/'),fSignature,1);
  44115. if i=0 then
  44116. result := '' else
  44117. result := copy(fSignature,1,i-1);
  44118. end;
  44119. procedure TSQLRecordSigned.UnSign;
  44120. begin
  44121. fSignature := '';
  44122. fSignatureTime := 0;
  44123. end;
  44124. { TSQLRecordInterfaced }
  44125. class function TSQLRecordInterfaced.NewInstance: TObject;
  44126. begin
  44127. result := inherited NewInstance;
  44128. TSQLRecordInterfaced(result).fRefCount := 1;
  44129. end;
  44130. procedure TSQLRecordInterfaced.AfterConstruction;
  44131. {$ifdef PUREPASCAL}
  44132. begin
  44133. InterlockedDecrement(fRefCount); // fRefCount=1 in NewInstance
  44134. end;
  44135. {$else}
  44136. asm
  44137. lock dec [eax].TInterfacedObject.fRefCount
  44138. end;
  44139. {$endif}
  44140. procedure TSQLRecordInterfaced.BeforeDestruction;
  44141. begin
  44142. if fRefCount<>0 then
  44143. System.Error(reInvalidPtr);
  44144. end;
  44145. {$ifdef FPC}
  44146. function TSQLRecordInterfaced.QueryInterface(
  44147. {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID; out Obj): longint;
  44148. {$else}
  44149. function TSQLRecordInterfaced.QueryInterface(const IID: TGUID; out Obj): HResult;
  44150. {$endif}
  44151. begin
  44152. if GetInterface(IID,Obj) then
  44153. result := 0 else
  44154. result := {$ifdef FPC}longint{$endif}(E_NOINTERFACE);
  44155. end;
  44156. function TSQLRecordInterfaced._AddRef: {$ifdef FPC}longint{$else}integer{$endif};
  44157. begin
  44158. result := InterlockedIncrement(fRefCount);
  44159. end;
  44160. function TSQLRecordInterfaced._Release: {$ifdef FPC}longint{$else}integer{$endif};
  44161. begin
  44162. result := InterlockedDecrement(fRefCount);
  44163. if result=0 then
  44164. Destroy;
  44165. end;
  44166. { TSQLRecordFTS3 }
  44167. class function TSQLRecordFTS3.OptimizeFTS3Index(Server: TSQLRestServer): boolean;
  44168. begin
  44169. if (self=nil) or (Server=nil) then
  44170. Result:= false else
  44171. with RecordProps do
  44172. Result := Server.ExecuteFmt('INSERT INTO %(%) VALUES(''optimize'');',
  44173. [SQLTableName,SQLTableName]);
  44174. end;
  44175. { TSQLRecordFTS4 }
  44176. class procedure TSQLRecordFTS4.InitializeTable(Server: TSQLRestServer;
  44177. const FieldName: RawUTF8; Options: TSQLInitializeTableOptions);
  44178. var Props: TSQLModelRecordProperties;
  44179. main,fts,ftsfields: RawUTF8;
  44180. begin
  44181. inherited;
  44182. if FieldName<>'' then
  44183. exit;
  44184. Props := Server.Model.Props[self];
  44185. if (Props=nil) or (Props.fFTSWithoutContentFields='') then
  44186. exit;
  44187. main := Server.Model.Tables[Props.fFTSWithoutContentTableIndex].SQLTableName;
  44188. if not Server.IsInternalSQLite3Table(Props.fFTSWithoutContentTableIndex) then begin
  44189. Server.InternalLog('% is an external content FTS4 table but source % is not '+
  44190. 'a local SQLite3 table: FTS search would be unavailable',[self,main],sllWarning);
  44191. exit;
  44192. end;
  44193. fts := Props.Props.SQLTableName;
  44194. ftsfields := Props.Props.SQLTableSimpleFieldsNoRowID;
  44195. // see http://www.sqlite.org/fts3.html#*fts4content
  44196. Server.ExecuteFmt('CREATE TRIGGER %_bu BEFORE UPDATE ON % '+
  44197. 'BEGIN DELETE FROM % WHERE docid=old.rowid; END;',
  44198. [main,main,fts]);
  44199. Server.ExecuteFmt('CREATE TRIGGER %_bd BEFORE DELETE ON % '+
  44200. 'BEGIN DELETE FROM % WHERE docid=old.rowid; END;',
  44201. [main,main,fts]);
  44202. Server.ExecuteFmt('CREATE TRIGGER %_au AFTER UPDATE ON % '+
  44203. 'BEGIN INSERT INTO %(docid,%) VALUES(new.rowid%); END;',
  44204. [main,main,fts,ftsfields,Props.fFTSWithoutContentFields]);
  44205. Server.ExecuteFmt('CREATE TRIGGER %_ai AFTER INSERT ON % '+
  44206. 'BEGIN INSERT INTO %(docid,%) VALUES(new.rowid%); END;',
  44207. [main,main,fts,ftsfields,Props.fFTSWithoutContentFields]);
  44208. end;
  44209. { TSQLRecordRTree }
  44210. class procedure TSQLRecordRTree.BlobToCoord(const InBlob;
  44211. var OutCoord: TSQLRecordTreeCoords);
  44212. begin // direct memory copy with no memory check
  44213. MoveFast(InBlob,OutCoord,(RecordProps.Fields.Count shr 1)*sizeof(double));
  44214. end;
  44215. class function TSQLRecordRTree.ContainedIn(const BlobA,BlobB): boolean;
  44216. var A,B: TSQLRecordTreeCoords;
  44217. i: integer;
  44218. begin
  44219. BlobToCoord(BlobA,A);
  44220. BlobToCoord(BlobB,B);
  44221. result := false;
  44222. for i := 0 to (RecordProps.Fields.Count shr 1)-1 do
  44223. if (A[i].max<B[i].min) or (A[i].min>B[i].max) then
  44224. exit; // no match
  44225. result := true; // box match
  44226. end;
  44227. class function TSQLRecordRTree.RTreeSQLFunctionName: RawUTF8;
  44228. begin
  44229. result := RecordProps.SQLTableName+'_in';
  44230. end;
  44231. { TSQLRecordMany }
  44232. constructor TSQLRecordMany.Create;
  44233. begin
  44234. inherited Create;
  44235. with RecordProps do
  44236. if (fRecordManySourceProp<>nil) and (fRecordManyDestProp<>nil) then begin
  44237. fSourceID := fRecordManySourceProp.GetFieldAddr(Self);
  44238. fDestID := fRecordManyDestProp.GetFieldAddr(Self);
  44239. end;
  44240. end;
  44241. function TSQLRecordMany.ManyAdd(aClient: TSQLRest; aSourceID, aDestID: TID;
  44242. NoDuplicates: boolean; aUseBatch: TSQLRestBatch): boolean;
  44243. begin
  44244. result := false;
  44245. if (self=nil) or (aClient=nil) or (aSourceID=0) or (aDestID=0) or
  44246. (fSourceID=nil) or (fDestID=nil) then
  44247. exit; // invalid parameters
  44248. if NoDuplicates and
  44249. (InternalIDFromSourceDest(aClient,aSourceID,aDestID)<>0) then
  44250. exit; // this TRecordReference pair already exists
  44251. fSourceID^ := aSourceID;
  44252. fDestID^ := aDestID;
  44253. if aUseBatch<>nil then
  44254. result := aUseBatch.Add(self,true)<>0 else
  44255. result := aClient.Add(self,true)<>0;
  44256. end;
  44257. function TSQLRecordMany.ManyAdd(aClient: TSQLRest; aDestID: TID;
  44258. NoDuplicates: boolean): boolean;
  44259. begin
  44260. if (self=nil) or (fSourceID=nil) then
  44261. result := false else // avoid GPF
  44262. result := ManyAdd(aClient,fSourceID^,aDestID,NoDuplicates);
  44263. end;
  44264. function TSQLRecordMany.DestGet(aClient: TSQLRest; aSourceID: TID;
  44265. out DestIDs: TIDDynArray): Boolean;
  44266. var Where: RawUTF8;
  44267. begin
  44268. Where := IDWhereSQL(aClient,aSourceID,False);
  44269. if Where='' then
  44270. result := False else
  44271. result := aClient.OneFieldValues(RecordClass,'Dest',Where,TInt64DynArray(DestIDs));
  44272. end;
  44273. function TSQLRecordMany.DestGetJoined(aClient: TSQLRest;
  44274. const aDestWhereSQL: RawUTF8; aSourceID: TID;
  44275. out DestIDs: TIDDynArray): boolean;
  44276. var aTable: TSQLTable;
  44277. begin
  44278. aTable := DestGetJoinedTable(aClient,aDestWhereSQL,aSourceID,jkDestID);
  44279. if aTable=nil then
  44280. Result := False else
  44281. try
  44282. aTable.GetRowValues(0,TInt64DynArray(DestIDs));
  44283. Result := true;
  44284. finally
  44285. aTable.Free;
  44286. end;
  44287. end;
  44288. function TSQLRecordMany.DestGetJoined(aClient: TSQLRest;
  44289. const aDestWhereSQL: RawUTF8; aSourceID: TID): TSQLRecord;
  44290. var aTable: TSQLTable;
  44291. begin
  44292. aTable := DestGetJoinedTable(aClient,aDestWhereSQL,aSourceID,jkDestFields);
  44293. if aTable=nil then
  44294. Result := nil else begin
  44295. Result := TSQLRecordClass(RecordProps.fRecordManyDestProp.ObjectClass).Create;
  44296. aTable.OwnerMustFree := true;
  44297. Result.FillPrepare(aTable,ctnTrimExisting);
  44298. end;
  44299. end;
  44300. function TSQLRecordMany.DestGetJoinedTable(aClient: TSQLRest;
  44301. const aDestWhereSQL: RawUTF8; aSourceID: TID; JoinKind: TSQLRecordManyJoinKind;
  44302. const aCustomFieldsCSV: RawUTF8): TSQLTable;
  44303. var Select, SQL: RawUTF8;
  44304. SelfProps, DestProps: TSQLModelRecordProperties;
  44305. procedure SelectFields(const Classes: array of TSQLModelRecordProperties);
  44306. var i: integer;
  44307. begin
  44308. for i := 0 to high(Classes) do begin
  44309. Select := Select+Classes[i].SQL.TableSimpleFields[True,True];
  44310. if i<high(Classes) then
  44311. Select := Select+',';
  44312. end;
  44313. end;
  44314. begin
  44315. result := nil;
  44316. if (Self=nil) or (fSourceID=nil) or (fDestID=nil) or (aClient=nil) then
  44317. exit;
  44318. if aSourceID=0 then
  44319. if fSourceID<>nil then
  44320. aSourceID := fSourceID^;
  44321. if aSourceID=0 then
  44322. exit;
  44323. SelfProps := aClient.Model.Props[PSQLRecordClass(self)^];
  44324. DestProps := aClient.Model.Props[TSQLRecordClass(SelfProps.Props.fRecordManyDestProp.ObjectClass)];
  44325. case JoinKind of
  44326. jkDestID:
  44327. Select := DestProps.Props.SQLTableName+'.RowID';
  44328. jkPivotID:
  44329. Select := SelfProps.Props.SQLTableName+'.RowID';
  44330. jkDestFields:
  44331. if aCustomFieldsCSV='' then
  44332. SelectFields([DestProps]) else
  44333. Select := AddPrefixToCSV(pointer(aCustomFieldsCSV),DestProps.Props.SQLTableName+'.');
  44334. jkPivotFields:
  44335. if aCustomFieldsCSV='' then
  44336. SelectFields([SelfProps]) else
  44337. Select := AddPrefixToCSV(pointer(aCustomFieldsCSV),SelfProps.Props.SQLTableName+'.');
  44338. jkPivotAndDestFields:
  44339. if aCustomFieldsCSV='' then
  44340. SelectFields([SelfProps,DestProps]) else
  44341. Select := aCustomFieldsCSV;
  44342. end;
  44343. if aDestWhereSQL='' then
  44344. // fast inlined prepared statement
  44345. SQL := 'SELECT % FROM %,% WHERE %.Source=:(%): AND %.Dest=%.RowID' else
  44346. if PosEx(RawUTF8(':('),aDestWhereSQL,1)>0 then
  44347. // statement is globaly inlined -> cache prepared statement
  44348. SQL := 'SELECT % FROM %,% WHERE %.Source=:(%): AND %.Dest=%.RowID AND %' else
  44349. // statement is not globaly inlined -> no caching of prepared statement
  44350. SQL := 'SELECT % FROM %,% WHERE %.Source=% AND %.Dest=%.RowID AND %';
  44351. result := aClient.ExecuteList([PSQLRecordClass(Self)^,
  44352. TSQLRecordClass(SelfProps.Props.fRecordManyDestProp.ObjectClass)],
  44353. FormatUTF8(SQL,
  44354. [Select, DestProps.Props.SQLTableName,SelfProps.Props.SQLTableName,
  44355. SelfProps.Props.SQLTableName,aSourceID, SelfProps.Props.SQLTableName,
  44356. DestProps.Props.SQLTableName, aDestWhereSQL]));
  44357. end;
  44358. function TSQLRecordMany.DestGet(aClient: TSQLRest;
  44359. out DestIDs: TIDDynArray): boolean;
  44360. begin
  44361. if fSourceID=nil then
  44362. result := false else // avoid GPF
  44363. result := DestGet(aClient,fSourceID^,DestIDs);
  44364. // fSourceID has been set by TSQLRecord.Create
  44365. end;
  44366. function TSQLRecordMany.ManyDelete(aClient: TSQLRest; aSourceID, aDestID: TID;
  44367. aUseBatch: TSQLRestBatch): boolean;
  44368. var aID: TID;
  44369. begin
  44370. result := false;
  44371. if (self=nil) or (aClient=nil) or (aSourceID=0) or (aDestID=0) then
  44372. exit;
  44373. aID := InternalIDFromSourceDest(aClient,aSourceID,aDestID);
  44374. if aID<>0 then
  44375. if aUseBatch<>nil then
  44376. result := aUseBatch.Delete(RecordClass,aID)>=0 else
  44377. result := aClient.Delete(RecordClass,aID);
  44378. end;
  44379. function TSQLRecordMany.ManyDelete(aClient: TSQLRest; aDestID: TID): boolean;
  44380. begin
  44381. if fSourceID=nil then
  44382. result := false else // avoid GPF
  44383. result := ManyDelete(aClient,fSourceID^,aDestID,nil);
  44384. end;
  44385. function TSQLRecordMany.ManySelect(aClient: TSQLRest; aSourceID, aDestID: TID): boolean;
  44386. begin
  44387. if (self=nil) or (aClient=nil) or (aSourceID=0) or (aDestID=0) then
  44388. result := false else // invalid parameters
  44389. result := aClient.Retrieve(FormatUTF8('Source=:(%): AND Dest=:(%):',
  44390. [aSourceID,aDestID]),Self);
  44391. end;
  44392. function TSQLRecordMany.ManySelect(aClient: TSQLRest; aDestID: TID): boolean;
  44393. begin
  44394. if (self=nil) or (fSourceID=nil) then
  44395. result := false else // avoid GPF
  44396. result := ManySelect(aClient,fSourceID^,aDestID);
  44397. end;
  44398. function TSQLRecordMany.InternalFillMany(aClient: TSQLRest;
  44399. aID: TID; const aAndWhereSQL: RawUTF8; isDest: boolean): integer;
  44400. var aTable: TSQLTable;
  44401. Where: RawUTF8;
  44402. begin
  44403. result := 0;
  44404. if self=nil then
  44405. exit;
  44406. if not isDest and (aID=0)then
  44407. if fSourceID<>nil then
  44408. aID := fSourceID^; // has been set by TSQLRecord.Create
  44409. Where := IDWhereSQL(aClient,aID,isDest,aAndWhereSQL);
  44410. if Where='' then
  44411. exit;
  44412. aTable := aClient.MultiFieldValues(RecordClass,'',Where);
  44413. if aTable=nil then
  44414. exit;
  44415. aTable.OwnerMustFree := true;
  44416. FillPrepare(aTable); // temporary storage for FillRow, FillOne and FillRewind
  44417. result := aTable.fRowCount;
  44418. end;
  44419. function TSQLRecordMany.FillMany(aClient: TSQLRest; aSourceID: TID;
  44420. const aAndWhereSQL: RawUTF8): integer;
  44421. begin
  44422. result := InternalFillMany(aclient,aSourceID,aAndWhereSQL,false);
  44423. end;
  44424. function TSQLRecordMany.FillManyFromDest(aClient: TSQLRest; aDestID: TID;
  44425. const aAndWhereSQL: RawUTF8): integer;
  44426. begin
  44427. result := InternalFillMany(aclient,aDestID,aAndWhereSQL,true);
  44428. end;
  44429. function TSQLRecordMany.IDWhereSQL(aClient: TSQLRest; aID: TID; isDest: boolean;
  44430. const aAndWhereSQL: RawUTF8=''): RawUTF8;
  44431. const FieldName: array[boolean] of RawUTF8 = ('Source=','Dest=');
  44432. begin
  44433. if (self=nil) or (aID=0) or (fSourceID=nil) or (fDestID=nil) or
  44434. (aClient=nil) then
  44435. Result := '' else begin
  44436. if aAndWhereSQL<>'' then
  44437. if PosEx(RawUTF8(':('),aAndWhereSQL,1)>0 then
  44438. Result := '%:(%): AND %' else // inlined parameters
  44439. Result := '%% AND %' else // no inlined parameters -> not cached
  44440. Result := '%:(%):'; // no additional where clause -> inline ID
  44441. Result := FormatUTF8(result,[FieldName[isDest],aID,aAndWhereSQL]);
  44442. end;
  44443. end;
  44444. function TSQLRecordMany.SourceGet(aClient: TSQLRest; aDestID: TID;
  44445. out SourceIDs: TIDDynArray): boolean;
  44446. var Where: RawUTF8;
  44447. begin
  44448. Where := IDWhereSQL(aClient,aDestID,True);
  44449. if Where='' then
  44450. Result := false else
  44451. Result := aClient.OneFieldValues(RecordClass,'Source',Where,TInt64DynArray(SourceIDs));
  44452. end;
  44453. function TSQLRecordMany.InternalIDFromSourceDest(aClient: TSQLRest;
  44454. aSourceID, aDestID: TID): TID;
  44455. begin
  44456. SetID(aClient.OneFieldValue(RecordClass,'RowID',
  44457. FormatUTF8('Source=:(%): AND Dest=:(%):',[aSourceID,aDestID])),result);
  44458. end;
  44459. { TSQLRestTempStorage }
  44460. constructor TSQLRestTempStorage.Create(aClass: TSQLRecordClass);
  44461. begin
  44462. inherited Create;
  44463. fStoredClass := aClass;
  44464. fStoredClassRecordProps := aClass.RecordProps;
  44465. fItems.InitSpecific(
  44466. TypeInfo(TSQLRestTempStorageItemDynArray),fItem,djInt64,@fCount);
  44467. fItems.Sorted := true;
  44468. // space for 524287 fake items (our sorted array would not like bigger extent)
  44469. fLastFakeID := $100000000000;
  44470. end;
  44471. destructor TSQLRestTempStorage.Destroy;
  44472. var i: integer;
  44473. begin
  44474. for i := 0 to fCount-1 do
  44475. fItem[i].Value.Free;
  44476. inherited;
  44477. end;
  44478. procedure TSQLRestTempStorage.InternalAddItem(const item: TSQLRestTempStorageItem);
  44479. begin
  44480. fItems.Add(item);
  44481. if (fCount>1) and (fItem[fCount-2].ID>item.ID) then
  44482. fItems.Sort else // ensure IDs are in increasing order
  44483. fItems.Sorted := true; // pessimistic fItems.Add() did reset to false
  44484. end;
  44485. function TSQLRestTempStorage.InternalSetFields(const FieldNames: RawUTF8;
  44486. out Fields: TSQLFieldBits): Boolean;
  44487. begin
  44488. if FieldNames='' then
  44489. Fields := fStoredClassRecordProps.SimpleFieldsBits[soUpdate] else
  44490. if FieldNames='*' then
  44491. FillcharFast(Fields,sizeof(Fields),255) else
  44492. if not fStoredClassRecordProps.FieldBitsFromCSV(FieldNames,Fields) then begin
  44493. result := false; // invalid FieldNames content
  44494. exit;
  44495. end;
  44496. result := True;
  44497. end;
  44498. function TSQLRestTempStorage.AddCopy(Value: TSQLRecord;
  44499. ForceID: boolean; const FieldNames: RawUTF8): TID;
  44500. begin
  44501. if (self=nil) or (Value=nil) then
  44502. result := 0 else
  44503. result := AddOwned(Value.CreateCopy,ForceID,FieldNames);
  44504. end;
  44505. function TSQLRestTempStorage.AddOwned(Value: TSQLRecord; ForceID: boolean;
  44506. const Fields: TSQLFieldBits): TID;
  44507. var item: TSQLRestTempStorageItem;
  44508. begin
  44509. result := 0;
  44510. if (self=nil) or (Value=nil) or
  44511. (ForceID and (Value.IDValue=0)) or
  44512. IsZero(Fields) then
  44513. exit;
  44514. item.ValueFields := Fields;
  44515. fSafe.Lock;
  44516. try
  44517. if ForceID then begin
  44518. item.ID := Value.IDValue;
  44519. if fItems.Find(item)>=0 then begin
  44520. Value.Free; // avoid memory leak
  44521. exit; // this forced ID is already existing!
  44522. end;
  44523. item.Kind := [itemInsert];
  44524. end else begin
  44525. inc(fLastFakeID);
  44526. item.ID := fLastFakeID;
  44527. Value.IDValue := fLastFakeID;
  44528. item.Kind := [itemInsert,itemFakeID];
  44529. end;
  44530. item.Value := Value; // instance will be owned by the list
  44531. InternalAddItem(item);
  44532. finally
  44533. Safe.UnLock;
  44534. end;
  44535. result := item.ID;
  44536. end;
  44537. function TSQLRestTempStorage.AddOwned(Value: TSQLRecord;
  44538. ForceID: boolean; const FieldNames: RawUTF8): TID;
  44539. var fields: TSQLFieldBits;
  44540. begin
  44541. if (self=nil) or not InternalSetFields(FieldNames,fields) then
  44542. result := 0 else
  44543. result := AddOwned(Value,ForceID,fields);
  44544. end;
  44545. procedure TSQLRestTempStorage.Delete(const ID: TID);
  44546. var i: integer;
  44547. item: TSQLRestTempStorageItem;
  44548. begin
  44549. if (self=nil) or (ID=0) then
  44550. exit;
  44551. fSafe.Lock;
  44552. try
  44553. i := fItems.Find(ID);
  44554. if i>=0 then
  44555. with fItem[i] do begin
  44556. FreeAndNil(Value); // Value=nil indicates deleted reord
  44557. if itemInsert in Kind then
  44558. fItems.Delete(i); // Add + Delete in place -> ignore this entry
  44559. exit;
  44560. end;
  44561. item.ID := ID;
  44562. item.Value := nil; // Value=nil indicates deleted record
  44563. FillZero(item.ValueFields);
  44564. InternalAddItem(item);
  44565. finally
  44566. Safe.UnLock;
  44567. end;
  44568. end;
  44569. function TSQLRestTempStorage.Update(Value: TSQLRecord;
  44570. const Fields: TSQLFieldBits): boolean;
  44571. var i,f: integer;
  44572. item: TSQLRestTempStorageItem;
  44573. existing: ^TSQLRestTempStorageItem;
  44574. begin
  44575. result := false;
  44576. if (self=nil) or (Value=nil) or (Value.IDValue=0) or
  44577. IsZero(fields) then
  44578. exit;
  44579. item.ID := Value.IDValue;
  44580. item.ValueFields := Fields;
  44581. fSafe.Lock;
  44582. try
  44583. i := fItems.Find(item);
  44584. if i>=0 then begin
  44585. existing := @fItem[i];
  44586. if existing.Value=nil then
  44587. exit; // impossible to update a deleted record
  44588. existing^.ValueFields := existing^.ValueFields+item.ValueFields;
  44589. for f := 0 to fStoredClassRecordProps.Fields.Count-1 do
  44590. if f in item.ValueFields then
  44591. fStoredClassRecordProps.Fields.List[f].CopyValue(Value,existing^.Value);
  44592. end else begin
  44593. item.Value := Value.CreateCopy;
  44594. FillZero(item.ValueFields);
  44595. InternalAddItem(item);
  44596. end;
  44597. result := true;
  44598. finally
  44599. Safe.UnLock;
  44600. end;
  44601. end;
  44602. function TSQLRestTempStorage.Update(Value: TSQLRecord;
  44603. const FieldNames: RawUTF8): boolean;
  44604. var fields: TSQLFieldBits;
  44605. begin
  44606. if (self<>nil) and InternalSetFields(FieldNames,fields) then
  44607. result := Update(Value,fields) else
  44608. result := false;
  44609. end;
  44610. function TSQLRestTempStorage.FlushAsBatch(Rest: TSQLRest;
  44611. AutomaticTransactionPerRow: cardinal): TSQLRestBatch;
  44612. var i: integer;
  44613. begin
  44614. if (self=nil) or (fCount=0) then begin
  44615. result := nil;
  44616. exit;
  44617. end;
  44618. result := TSQLRestBatch.Create(Rest,fStoredClass,AutomaticTransactionPerRow,[]);
  44619. fSafe.Lock;
  44620. try
  44621. for i := 0 to fCount-1 do
  44622. with fItem[i] do
  44623. if Value=nil then
  44624. result.Delete(ID) else begin
  44625. if itemInsert in Kind then
  44626. result.Add(Value,true,not(itemFakeID in Kind),ValueFields) else
  44627. result.Update(Value,ValueFields);
  44628. FreeAndNil(Value);
  44629. end;
  44630. fItems.Clear;
  44631. finally
  44632. Safe.UnLock;
  44633. end;
  44634. end;
  44635. function TSQLRestTempStorage.FromEvent(Event: TSQLEvent; ID: TID;
  44636. const JSON: RawUTF8): boolean;
  44637. var Value: TSQLRecord;
  44638. fields: TSQLFieldBits;
  44639. begin
  44640. if (self=nil) or (ID=0) then begin
  44641. result := false;
  44642. exit;
  44643. end;
  44644. if Event=seDelete then begin
  44645. Delete(ID);
  44646. result := true;
  44647. exit;
  44648. end;
  44649. Value := fStoredClass.Create;
  44650. try
  44651. Value.FillFrom(JSON,@fields);
  44652. Value.IDValue := ID;
  44653. case Event of
  44654. seAdd: begin
  44655. result := AddOwned(Value,True,fields)<>0;
  44656. Value := nil; // owned by the list
  44657. end;
  44658. seUpdate,seUpdateBlob:
  44659. result := Update(Value,fields);
  44660. else result := false;
  44661. end;
  44662. finally
  44663. Value.Free;
  44664. end;
  44665. end;
  44666. { TSQLRecordProperties }
  44667. procedure TSQLRecordProperties.InternalRegisterModel(aModel: TSQLModel;
  44668. aTableIndex: integer; aProperties: TSQLModelRecordProperties);
  44669. var i: integer;
  44670. begin
  44671. //assert(aTableIndex>=0);
  44672. EnterCriticalSection(fLock); // may be called from several threads at once
  44673. try
  44674. for i := 0 to fModelMax do
  44675. if fModel[i].Model=aModel then
  44676. exit; // already registered
  44677. inc(fModelMax);
  44678. if fModelMax>=length(fModel) then
  44679. SetLength(fModel,fModelMax+4);
  44680. with fModel[fModelMax] do begin
  44681. Model := aModel;
  44682. Properties := aProperties;
  44683. TableIndex := aTableIndex;
  44684. end;
  44685. finally
  44686. LeaveCriticalSection(fLock);
  44687. end;
  44688. end;
  44689. const // the most ambigous keywords - others may be used as column names
  44690. SQLITE3_KEYWORDS = ' from where group in as ';
  44691. constructor TSQLRecordProperties.Create(aTable: TSQLRecordClass);
  44692. var i,j, nProps: integer;
  44693. nMany, nSQLRecord, nSimple, nDynArray, nBlob, nBlobCustom,
  44694. nCopiableFields: integer;
  44695. isTSQLRecordMany: boolean;
  44696. F: TSQLPropInfo;
  44697. label Simple, Small, Copiabl;
  44698. begin
  44699. InitializeCriticalSection(fLock);
  44700. if aTable=nil then
  44701. raise EModelException.Create('TSQLRecordProperties.Create(nil)');
  44702. // register for JSONToObject() and for TSQLPropInfoRTTITID.Create()
  44703. // (should have been done before in TSQLModel.Create/AddTable)
  44704. TJSONSerializer.RegisterClassForJSON(aTable);
  44705. // initialize internal structures
  44706. fModelMax := -1;
  44707. fTable := aTable;
  44708. fSQLTableName := GetDisplayNameFromClass(aTable);
  44709. fSQLTableNameUpperWithDot := SynCommons.UpperCase(SQLTableName)+'.';
  44710. isTSQLRecordMany := aTable.InheritsFrom(TSQLRecordMany);
  44711. // add properties to internal Fields list
  44712. fClassType := PTypeInfo(aTable.ClassInfo)^.ClassType;
  44713. fClassProp := InternalClassProp(aTable);
  44714. nProps := ClassFieldCountWithParents(aTable);
  44715. if nProps>MAX_SQLFIELDS_INCLUDINGID then
  44716. raise EModelException.CreateUTF8('% has too many fields: %>=%',
  44717. [Table,nProps,MAX_SQLFIELDS]);
  44718. fFields := TSQLPropInfoList.Create(aTable,[pilRaiseEORMExceptionIfNotHandled]);
  44719. aTable.InternalRegisterCustomProperties(self);
  44720. if Fields.Count>MAX_SQLFIELDS_INCLUDINGID then
  44721. raise EModelException.CreateUTF8(
  44722. '% has too many fields after InternalRegisterCustomProperties(%): %>=%',
  44723. [Table,self,Fields.Count,MAX_SQLFIELDS]);
  44724. SetLength(Fields.fList,Fields.Count);
  44725. // generate some internal lookup information
  44726. fSQLTableRetrieveAllFields := 'ID';
  44727. SetLength(fManyFields,MAX_SQLFIELDS);
  44728. SetLength(fSimpleFields,MAX_SQLFIELDS);
  44729. SetLength(fJoinedFields,MAX_SQLFIELDS);
  44730. SetLength(fCopiableFields,MAX_SQLFIELDS);
  44731. SetLength(fDynArrayFields,MAX_SQLFIELDS);
  44732. SetLength(fBlobCustomFields,MAX_SQLFIELDS);
  44733. SetLength(fBlobFields,MAX_SQLFIELDS);
  44734. MainField[false] := -1;
  44735. MainField[true] := -1;
  44736. nMany := 0;
  44737. nSimple := 0;
  44738. nSQLRecord := 0;
  44739. nCopiableFields := 0;
  44740. nDynArray := 0;
  44741. nBlob := 0;
  44742. nBlobCustom := 0;
  44743. for i := 0 to Fields.Count-1 do begin
  44744. F := Fields.List[i];
  44745. // check field name
  44746. if IsRowID(pointer(F.Name)) then
  44747. raise EORMException.CreateUTF8('ID is already defined in TSQLRecord: '+
  44748. '%.% field name is not allowed as published property',[Table,F.Name]);
  44749. if PosEx(' '+LowerCase(F.Name)+' ',SQLITE3_KEYWORDS)>0 then
  44750. raise EORMException.CreateUTF8('%.% field name conflicts with a SQL keyword',[Table,F.Name]);
  44751. // handle unique fields, i.e. if marked as "stored false"
  44752. if aIsUnique in F.Attributes then begin
  44753. include(IsUniqueFieldsBits,i);
  44754. // must trim() text value before storage, and validate for unicity
  44755. if F.SQLFieldType in [sftUTF8Text,sftAnsiText] then
  44756. AddFilterOrValidate(i,TSynFilterTrim.Create);
  44757. AddFilterOrValidate(i,TSynValidateUniqueField.Create);
  44758. end;
  44759. // get corresponding properties content
  44760. include(fHasTypeFields,F.SQLFieldType);
  44761. include(FieldBits[F.SQLFieldType],i);
  44762. case F.SQLFieldType of
  44763. sftUnknown: ;
  44764. sftUTF8Text: begin
  44765. if aIsUnique in F.Attributes then
  44766. if MainField[false]<0 then
  44767. MainField[false] := i;
  44768. if MainField[true]<0 then
  44769. MainField[true] := i;
  44770. goto Small;
  44771. end;
  44772. sftBlob: begin
  44773. BlobFields[nBlob] := F as TSQLPropInfoRTTI;
  44774. inc(nBlob);
  44775. fSQLTableUpdateBlobFields := fSQLTableUpdateBlobFields+F.Name+'=?,';
  44776. fSQLTableRetrieveBlobFields := fSQLTableRetrieveBlobFields+F.Name+',';
  44777. fSQLTableRetrieveAllFields := fSQLTableRetrieveAllFields+','+F.Name;
  44778. goto Copiabl;
  44779. end;
  44780. sftID: // = TSQLRecord(aID)
  44781. if isTSQLRecordMany and
  44782. (IdemPropNameU(F.Name,'Source') or IdemPropNameU(F.Name,'Dest')) then
  44783. goto Small else begin
  44784. JoinedFields[nSQLRecord] := F as TSQLPropInfoRTTIID;
  44785. inc(nSQLRecord);
  44786. goto Small;
  44787. end;
  44788. sftMany: begin
  44789. ManyFields[nMany] := F as TSQLPropInfoRTTIMany;
  44790. inc(nMany);
  44791. end;
  44792. sftBlobDynArray:
  44793. with F as TSQLPropInfoRTTIDynArray do begin
  44794. if DynArrayIndex>0 then
  44795. for j := 0 to nDynArray-1 do
  44796. if DynArrayFields[j].DynArrayIndex=DynArrayIndex then
  44797. raise EModelException.CreateUTF8('dup index % for %.% and %.% properties',
  44798. [DynArrayIndex,Table,Name,Table,DynArrayFields[j].Name]);
  44799. DynArrayFields[nDynArray] := TSQLPropInfoRTTIDynArray(F);
  44800. if TSQLPropInfoRTTIDynArray(F).ObjArray<>nil then
  44801. fDynArrayFieldsHasObjArray := true;
  44802. inc(nDynArray);
  44803. goto Simple;
  44804. end;
  44805. sftBlobCustom, sftUTF8Custom: begin
  44806. BlobCustomFields[nBlobCustom] := F;
  44807. inc(nBlobCustom);
  44808. goto Simple;
  44809. end;
  44810. sftCreateTime: begin
  44811. include(ComputeBeforeAddFieldsBits,i);
  44812. goto Small;
  44813. end;
  44814. sftModTime, sftSessionUserID: begin
  44815. include(ComputeBeforeAddFieldsBits,i);
  44816. include(ComputeBeforeUpdateFieldsBits,i);
  44817. goto Small;
  44818. end;
  44819. sftRecordVersion: begin
  44820. if fRecordVersionField<>nil then
  44821. raise EModelException.CreateUTF8('%: only a single TRecordVersion '+
  44822. 'field is allowed per class',[Table]);
  44823. fRecordVersionField := F as TSQLPropInfoRTTIRecordVersion;
  44824. fSQLTableRetrieveAllFields := fSQLTableRetrieveAllFields+','+F.Name;
  44825. goto Copiabl;
  44826. end; // TRecordVersion is a copiable but not a simple field!
  44827. sftVariant: // sftNullable are included in SmallfieldsBits
  44828. goto Simple;
  44829. else begin
  44830. Small: include(SmallFieldsBits,i);
  44831. // this code follows NOT_SIMPLE_FIELDS/COPIABLE_FIELDS constants
  44832. Simple: SimpleFields[nSimple] := F;
  44833. inc(nSimple);
  44834. include(SimpleFieldsBits[soSelect],i);
  44835. fSQLTableSimpleFieldsNoRowID := fSQLTableSimpleFieldsNoRowID+F.Name+',';
  44836. fSQLTableRetrieveAllFields := fSQLTableRetrieveAllFields+','+F.Name;
  44837. Copiabl:include(CopiableFieldsBits,i);
  44838. CopiableFields[nCopiableFields] := F;
  44839. inc(nCopiableFields);
  44840. end;
  44841. end;
  44842. end;
  44843. if fSQLTableSimpleFieldsNoRowID<>'' then
  44844. SetLength(fSQLTableSimpleFieldsNoRowID,length(fSQLTableSimpleFieldsNoRowID)-1);
  44845. if fSQLTableUpdateBlobFields<>'' then
  44846. SetLength(fSQLTableUpdateBlobFields,length(fSQLTableUpdateBlobFields)-1);
  44847. if fSQLTableRetrieveBlobFields<>'' then
  44848. SetLength(fSQLTableRetrieveBlobFields,length(fSQLTableRetrieveBlobFields)-1);
  44849. SetLength(fManyFields,nMany);
  44850. SetLength(fSimpleFields,nSimple);
  44851. SetLength(fJoinedFields,nSQLRecord);
  44852. if nSQLRecord>0 then begin
  44853. SetLength(fJoinedFieldsTable,nSQLRecord+1);
  44854. fJoinedFieldsTable[0] := aTable;
  44855. for i := 0 to nSQLRecord-1 do
  44856. fJoinedFieldsTable[i+1] := TSQLRecordClass(JoinedFields[i].ObjectClass);
  44857. end;
  44858. SetLength(fCopiableFields,nCopiableFields);
  44859. SetLength(fDynArrayFields,nDynArray);
  44860. SetLength(fBlobCustomFields,nBlobCustom);
  44861. SetLength(fBlobFields,nBlob);
  44862. SimpleFieldsBits[soInsert] := SimpleFieldsBits[soSelect];
  44863. SimpleFieldsBits[soUpdate] := SimpleFieldsBits[soSelect];
  44864. SimpleFieldsBits[soDelete] := SimpleFieldsBits[soSelect];
  44865. SimpleFieldsCount[soInsert] := nSimple;
  44866. SimpleFieldsCount[soUpdate] := nSimple;
  44867. SimpleFieldsCount[soDelete] := nSimple;
  44868. fHasNotSimpleFields := nSimple<>Fields.Count;
  44869. for i := 0 to Fields.Count-1 do
  44870. if Fields.List[i].SQLFieldType=sftCreateTime then begin
  44871. exclude(SimpleFieldsBits[soUpdate],i);
  44872. dec(SimpleFieldsCount[soUpdate]);
  44873. end;
  44874. if SmallFieldsBits<>SimpleFieldsBits[soSelect]-FieldBits[sftVariant]-
  44875. FieldBits[sftBlobDynArray]-FieldBits[sftBlobCustom]-FieldBits[sftUTF8Custom] then
  44876. raise EModelException.CreateUTF8('TSQLRecordProperties.Create(%) Bits?',[Table]);
  44877. if isTSQLRecordMany then begin
  44878. fRecordManySourceProp := Fields.ByRawUTF8Name('Source') as TSQLPropInfoRTTIInstance;
  44879. if fRecordManySourceProp=nil then
  44880. raise EModelException.CreateUTF8('% expects a SOURCE field',[Table]) else
  44881. fRecordManyDestProp := Fields.ByRawUTF8Name('Dest') as TSQLPropInfoRTTIInstance;
  44882. if fRecordManyDestProp=nil then
  44883. raise EModelException.CreateUTF8('% expects a DEST field',[Table]);
  44884. end;
  44885. end;
  44886. function TSQLRecordProperties.BlobFieldPropFromRawUTF8(const PropName: RawUTF8): PPropInfo;
  44887. var i: integer;
  44888. begin
  44889. if (self<>nil) and (PropName<>'') then
  44890. for i := 0 to high(BlobFields) do
  44891. if IdemPropNameU(BlobFields[i].Name,PropName) then begin
  44892. result := BlobFields[i].PropInfo;
  44893. exit;
  44894. end;
  44895. result := nil;
  44896. end;
  44897. function TSQLRecordProperties.BlobFieldPropFromUTF8(PropName: PUTF8Char; PropNameLen: integer): PPropInfo;
  44898. var i: integer;
  44899. begin
  44900. if (self<>nil) and (PropName<>'') then
  44901. for i := 0 to high(BlobFields) do
  44902. if IdemPropName(BlobFields[i].PropInfo^.Name,PropName,PropNameLen) then begin
  44903. result := BlobFields[i].PropInfo;
  44904. exit;
  44905. end;
  44906. result := nil;
  44907. end;
  44908. const
  44909. DBTOFIELDTYPE: array[TSQLDBFieldType] of TSQLFieldType = (sftUnknown,
  44910. sftUnknown,sftInteger,sftFloat,sftCurrency,sftDateTime,sftUTF8Text,sftBlob);
  44911. function TSQLRecordProperties.SQLFieldTypeToSQL(FieldIndex: integer): RawUTF8;
  44912. const
  44913. /// simple wrapper from each SQL used type into SQLite3 field datatype
  44914. // - set to '' for fields with no column created in the database
  44915. DEFAULT_SQLFIELDTYPETOSQL: array[TSQLFieldType] of RawUTF8 =
  44916. ('', // sftUnknown
  44917. ' TEXT COLLATE NOCASE, ', // sftAnsiText
  44918. ' TEXT COLLATE SYSTEMNOCASE, ', // sftUTF8Text
  44919. ' INTEGER, ', // sftEnumerate
  44920. ' INTEGER, ', // sftSet
  44921. ' INTEGER, ', // sftInteger
  44922. ' INTEGER, ', // sftID = TSQLRecord(aID)
  44923. ' INTEGER, ', // sftRecord = TRecordReference
  44924. ' INTEGER, ', // sftBoolean
  44925. ' FLOAT, ', // sftFloat
  44926. ' TEXT COLLATE ISO8601, ', // sftDateTime
  44927. ' INTEGER, ', // sftTimeLog
  44928. ' FLOAT, ', // sftCurrency
  44929. ' TEXT COLLATE BINARY, ', // sftObject
  44930. {$ifndef NOVARIANTS}
  44931. ' TEXT COLLATE BINARY, ', // sftVariant
  44932. ' TEXT COLLATE NOCASE, ', // sftNullable (from SQLFieldTypeStored)
  44933. {$endif}
  44934. ' BLOB, ', // sftBlob
  44935. ' BLOB, ', // sftBlobDynArray
  44936. ' BLOB, ', // sftBlobCustom
  44937. ' TEXT COLLATE NOCASE, ', // sftUTF8Custom
  44938. '', // sftMany
  44939. ' INTEGER, ', // sftModTime
  44940. ' INTEGER, ', // sftCreateTime
  44941. ' INTEGER, ', // sftTID
  44942. ' INTEGER, ', // sftRecordVersion
  44943. ' INTEGER, '); // sftSessionUserID
  44944. begin
  44945. if (self=nil) or (cardinal(FieldIndex)>=cardinal(Fields.Count)) then
  44946. result := '' else
  44947. if (FieldIndex<length(fCustomCollation)) and (fCustomCollation[FieldIndex]<>'') then
  44948. result := ' TEXT COLLATE '+fCustomCollation[FieldIndex]+', ' else
  44949. result := DEFAULT_SQLFIELDTYPETOSQL[Fields.List[FieldIndex].SQLFieldTypeStored];
  44950. end;
  44951. function TSQLRecordProperties.SetCustomCollation(FieldIndex: integer; const aCollationName: RawUTF8): boolean;
  44952. begin
  44953. result := (self<>nil) and (cardinal(FieldIndex)<cardinal(Fields.Count));
  44954. if result then begin
  44955. if Fields.Count>length(fCustomCollation) then
  44956. SetLength(fCustomCollation,Fields.Count);
  44957. fCustomCollation[FieldIndex] := aCollationName;
  44958. end;
  44959. end;
  44960. function TSQLRecordProperties.SetCustomCollation(const aFieldName, aCollationName: RawUTF8): boolean;
  44961. begin
  44962. result := SetCustomCollation(Fields.IndexByNameOrExcept(aFieldName),aCollationName);
  44963. end;
  44964. procedure TSQLRecordProperties.SetCustomCollationForAll(aFieldType: TSQLFieldType;
  44965. const aCollationName: RawUTF8);
  44966. var i: integer;
  44967. begin
  44968. if (self=nil) or (aFieldType in [sftUnknown,sftMany]) then
  44969. exit;
  44970. if Fields.Count>length(fCustomCollation) then
  44971. SetLength(fCustomCollation,Fields.Count);
  44972. for i := 0 to Fields.Count-1 do
  44973. if Fields.List[i].SQLFieldTypeStored=aFieldType then
  44974. fCustomCollation[i] := aCollationName;
  44975. end;
  44976. procedure TSQLRecordProperties.SetMaxLengthValidatorForTextFields(IndexIsUTF8Length: boolean);
  44977. var i: integer;
  44978. begin
  44979. if self<>nil then
  44980. for i := 0 to Fields.Count-1 do
  44981. with Fields.List[i] do
  44982. if (SQLDBFieldType in TEXT_DBFIELDS) and (cardinal(FieldWidth-1)<262144) then
  44983. AddFilterOrValidate(i,TSynValidateText.CreateUTF8('{maxLength:%,UTF8Length:%}',
  44984. [FieldWidth,IndexIsUTF8Length],[]));
  44985. end;
  44986. procedure TSQLRecordProperties.SetMaxLengthFilterForTextFields(IndexIsUTF8Length: boolean);
  44987. var i: integer;
  44988. begin
  44989. if self<>nil then
  44990. for i := 0 to Fields.Count-1 do
  44991. with Fields.List[i] do
  44992. if (SQLDBFieldType in TEXT_DBFIELDS) and (cardinal(FieldWidth-1)<262144) then
  44993. AddFilterOrValidate(i,TSynFilterTruncate.CreateUTF8('{maxLength:%,UTF8Length:%}',
  44994. [FieldWidth,IndexIsUTF8Length],[]));
  44995. end;
  44996. {$ifndef NOVARIANTS}
  44997. procedure TSQLRecordProperties.SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions);
  44998. var i: integer;
  44999. begin
  45000. if self<>nil then
  45001. for i := 0 to Fields.Count-1 do
  45002. if (Fields.List[i].SQLFieldType=sftVariant) and
  45003. Fields.List[i].InheritsFrom(TSQLPropInfoRTTIVariant) then
  45004. TSQLPropInfoRTTIVariant(Fields.List[i]).DocVariantOptions := Options;
  45005. end;
  45006. {$endif}
  45007. function TSQLRecordProperties.SQLAddField(FieldIndex: integer): RawUTF8;
  45008. begin
  45009. if (self=nil) or (cardinal(FieldIndex)>=cardinal(Fields.Count)) then begin
  45010. result := '';
  45011. exit;
  45012. end;
  45013. result := SQLFieldTypeToSQL(FieldIndex);
  45014. if result='' then
  45015. exit; // some fields won't have any column created in the database
  45016. result := FormatUTF8('ALTER TABLE % ADD COLUMN %%',
  45017. [SQLTableName,Fields.List[FieldIndex].Name,result]);
  45018. if FieldIndex in IsUniqueFieldsBits then
  45019. insert(' UNIQUE',result,length(result)-1);
  45020. result[length(result)-1] := ';' // SQLFieldTypeToSQL[] ends with ','
  45021. end;
  45022. procedure TSQLRecordProperties.SetJSONWriterColumnNames(W: TJSONSerializer;
  45023. KnownRowsCount: integer);
  45024. var i,n,nf: integer;
  45025. begin
  45026. // get col count overhead
  45027. if W.withID then
  45028. n := 1 else
  45029. n := 0;
  45030. // set col names
  45031. nf := Length(W.Fields);
  45032. SetLength(W.ColNames,nf+n);
  45033. if W.withID then
  45034. W.ColNames[0] := 'RowID'; // works for both normal and FTS3 records
  45035. for i := 0 to nf-1 do begin
  45036. W.ColNames[n] := Fields.List[W.Fields[i]].Name;
  45037. inc(n);
  45038. end;
  45039. // write or init field names for appropriate JSON Expand
  45040. W.AddColumns(KnownRowsCount);
  45041. end;
  45042. function TSQLRecordProperties.CreateJSONWriter(JSON: TStream; Expand,
  45043. withID: boolean; const aFields: TSQLFieldBits; KnownRowsCount: integer): TJSONSerializer;
  45044. begin
  45045. result := CreateJSONWriter(JSON,Expand,withID,
  45046. FieldBitsToIndex(aFields,Fields.Count),KnownRowsCount);
  45047. end;
  45048. function TSQLRecordProperties.CreateJSONWriter(JSON: TStream; Expand,
  45049. withID: boolean; const aFields: TSQLFieldIndexDynArray; KnownRowsCount: integer): TJSONSerializer;
  45050. begin
  45051. if (self=nil) or ((Fields=nil) and not withID) then // no data
  45052. result := nil else begin
  45053. result := TJSONSerializer.Create(JSON,Expand,withID,aFields);
  45054. SetJSONWriterColumnNames(result,KnownRowsCount);
  45055. end;
  45056. end;
  45057. function TSQLRecordProperties.CreateJSONWriter(JSON: TStream; Expand: boolean;
  45058. const aFieldsCSV: RawUTF8; KnownRowsCount: integer): TJSONSerializer;
  45059. var withID: boolean;
  45060. bits: TSQLFieldBits;
  45061. begin
  45062. FieldBitsFromCSV(aFieldsCSV,bits,withID);
  45063. result := CreateJSONWriter(JSON,Expand,withID,bits,KnownRowsCount);
  45064. end;
  45065. function TSQLRecordProperties.SaveSimpleFieldsFromJsonArray(var P: PUTF8Char;
  45066. var EndOfObject: AnsiChar; ExtendedJSON: boolean): RawUTF8;
  45067. var i: integer;
  45068. W: TJSONSerializer;
  45069. Start: PUTF8Char;
  45070. begin
  45071. result := '';
  45072. if P=nil then
  45073. exit;
  45074. if P^ in [#1..' '] then repeat inc(P) until not(P^ in [#1..' ']);
  45075. if P^<>'[' then
  45076. exit;
  45077. repeat inc(P) until not(P^ in [#1..' ']);
  45078. W := TJSONSerializer.CreateOwnedStream(1024);
  45079. try
  45080. W.Add('{');
  45081. for i := 0 to length(SimpleFields)-1 do begin
  45082. if ExtendedJSON then begin
  45083. W.AddString(SimpleFields[i].Name);
  45084. W.Add(':');
  45085. end else
  45086. W.AddFieldName(SimpleFields[i].Name);
  45087. Start := P;
  45088. P := GotoEndJSONItem(P);
  45089. if (P=nil) or not(P^ in [',',']']) then
  45090. exit;
  45091. W.AddNoJSONEscape(Start,P-Start);
  45092. W.Add(',');
  45093. repeat inc(P) until not(P^ in [#1..' ']);
  45094. end;
  45095. W.CancelLastComma;
  45096. W.Add('}');
  45097. W.SetText(result);
  45098. finally
  45099. W.Free;
  45100. end;
  45101. EndOfObject := P^;
  45102. if P^<>#0 then
  45103. repeat inc(P) until not(P^ in [#1..' ']);
  45104. end;
  45105. procedure TSQLRecordProperties.SaveBinaryHeader(W: TFileBufferWriter);
  45106. var i: integer;
  45107. FieldNames: TRawUTF8DynArray;
  45108. begin
  45109. W.Write(SQLTableName);
  45110. SetLength(FieldNames,Fields.Count);
  45111. for i := 0 to Fields.Count-1 do
  45112. FieldNames[i] := Fields.List[i].Name;
  45113. W.WriteRawUTF8DynArray(FieldNames,Fields.Count);
  45114. for i := 0 to Fields.Count-1 do
  45115. W.Write(@Fields.List[i].fSQLFieldType,sizeof(TSQLFieldType));
  45116. end;
  45117. function TSQLRecordProperties.CheckBinaryHeader(var R: TFileBufferReader): boolean;
  45118. var n,i: integer;
  45119. FieldNames: TRawUTF8DynArray;
  45120. FieldTypes: array[0..MAX_SQLFIELDS-1] of TSQLFieldType;
  45121. begin
  45122. result := false;
  45123. if (R.ReadRawUTF8<>SQLTableName) or
  45124. (R.ReadVarRawUTF8DynArray(FieldNames)<>Fields.Count) then
  45125. exit;
  45126. n := sizeof(TSQLFieldType)*Fields.Count;
  45127. if R.Read(@FieldTypes,n)<>n then
  45128. exit;
  45129. for i := 0 to Fields.Count-1 do
  45130. with Fields.List[i] do
  45131. if (Name<>FieldNames[i]) or (SQLFieldType<>FieldTypes[i]) then
  45132. exit;
  45133. result := true;
  45134. end;
  45135. function TSQLRecordProperties.IsFieldName(const PropName: RawUTF8): boolean;
  45136. begin
  45137. result := (PropName<>'') and
  45138. (isRowID(pointer(PropName)) or (Fields.IndexByName(PropName)>=0));
  45139. end;
  45140. function TSQLRecordProperties.IsFieldNameOrFunction(const PropName: RawUTF8): boolean;
  45141. var L: integer;
  45142. begin
  45143. L := length(PropName);
  45144. if (L=0) or (self=nil) then
  45145. result := false else
  45146. if (PropName[L]=')') and
  45147. (IdemPCharArray(pointer(PropName),['MAX(','MIN(','AVG(','SUM('])>=0) then
  45148. result := IsFieldName(copy(PropName,5,L-5)) else
  45149. result := IsFieldName(PropName);
  45150. end;
  45151. function TSQLRecordProperties.AddFilterOrValidate(aFieldIndex: integer;
  45152. aFilter: TSynFilterOrValidate): boolean;
  45153. begin
  45154. if (self=nil) or (cardinal(aFieldIndex)>=cardinal(Fields.Count)) or
  45155. (aFilter=nil) then
  45156. result := false else begin
  45157. if Filters=nil then
  45158. SetLength(fFilters,Fields.Count);
  45159. aFilter.AddOnce(Filters[aFieldIndex]);
  45160. result := true;
  45161. end;
  45162. end;
  45163. procedure TSQLRecordProperties.AddFilterOrValidate(const aFieldName: RawUTF8;
  45164. aFilter: TSynFilterOrValidate);
  45165. begin
  45166. AddFilterOrValidate(Fields.IndexByNameOrExcept(aFieldName),aFilter);
  45167. end;
  45168. destructor TSQLRecordProperties.Destroy;
  45169. var f: integer;
  45170. begin
  45171. for f := 0 to high(Filters) do
  45172. ObjArrayClear(Filters[f]); // will free any created TSynFilter instances
  45173. inherited;
  45174. DeleteCriticalSection(fLock);
  45175. Fields.Free;
  45176. end;
  45177. function TSQLRecordProperties.FieldBitsFromBlobField(aBlobField: PPropInfo;
  45178. var Bits: TSQLFieldBits): boolean;
  45179. var f: integer;
  45180. begin
  45181. FillZero(Bits);
  45182. if self<>nil then
  45183. for f := 0 to high(BlobFields) do
  45184. if BlobFields[f].fPropInfo=aBlobField then begin
  45185. Include(Bits,BlobFields[f].PropertyIndex);
  45186. result := true;
  45187. exit;
  45188. end;
  45189. result := false;
  45190. end;
  45191. function TSQLRecordProperties.FieldBitsFromCSV(const aFieldsCSV: RawUTF8;
  45192. var Bits: TSQLFieldBits): boolean;
  45193. var ndx: integer;
  45194. P: PUTF8Char;
  45195. FieldName: ShortString;
  45196. begin
  45197. FillZero(Bits);
  45198. result := false;
  45199. if self=nil then
  45200. exit;
  45201. P := pointer(aFieldsCSV);
  45202. while P<>nil do begin
  45203. GetNextItemShortString(P,FieldName);
  45204. FieldName[ord(FieldName[0])+1] := #0; // make PUTF8Char
  45205. ndx := Fields.IndexByName(@FieldName[1]);
  45206. if ndx<0 then
  45207. exit; // invalid field name
  45208. include(Bits,ndx);
  45209. end;
  45210. result := true;
  45211. end;
  45212. function TSQLRecordProperties.FieldBitsFromCSV(const aFieldsCSV: RawUTF8;
  45213. var Bits: TSQLFieldBits; out withID: boolean): boolean;
  45214. var ndx: integer;
  45215. P: PUTF8Char;
  45216. FieldName: ShortString;
  45217. begin
  45218. if (aFieldsCSV='*') and (self<>nil) then begin
  45219. Bits := SimpleFieldsBits[soSelect];
  45220. withID := true;
  45221. result := true;
  45222. exit;
  45223. end;
  45224. FillZero(Bits);
  45225. withID := false;
  45226. result := false;
  45227. if self=nil then
  45228. exit;
  45229. P := pointer(aFieldsCSV);
  45230. while P<>nil do begin
  45231. GetNextItemShortString(P,FieldName);
  45232. if IsRowIDShort(FieldName) then begin
  45233. withID := true;
  45234. continue;
  45235. end;
  45236. FieldName[ord(FieldName[0])+1] := #0; // make PUTF8Char
  45237. ndx := Fields.IndexByName(@FieldName[1]);
  45238. if ndx<0 then
  45239. exit; // invalid field name
  45240. include(Bits,ndx);
  45241. end;
  45242. result := true;
  45243. end;
  45244. function TSQLRecordProperties.FieldBitsFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldBits;
  45245. begin
  45246. if not FieldBitsFromCSV(aFieldsCSV,Result) then
  45247. FillZero(result);
  45248. end;
  45249. function TSQLRecordProperties.FieldBitsFromExcludingCSV(
  45250. const aFieldsCSV: RawUTF8; aOccasion: TSQLOccasion): TSQLFieldBits;
  45251. var excluded: TSQLFieldBits;
  45252. begin
  45253. result := SimpleFieldsBits[aOccasion];
  45254. if FieldBitsFromCSV(aFieldsCSV,excluded) then
  45255. result := result-excluded;
  45256. end;
  45257. function TSQLRecordProperties.FieldBitsFromRawUTF8(const aFields: array of RawUTF8;
  45258. var Bits: TSQLFieldBits): boolean;
  45259. var f,ndx: integer;
  45260. begin
  45261. FillZero(Bits);
  45262. result := false;
  45263. if self=nil then
  45264. exit;
  45265. for f := 0 to high(aFields) do begin
  45266. ndx := Fields.IndexByName(aFields[f]);
  45267. if ndx<0 then
  45268. exit; // invalid field name
  45269. include(Bits,ndx);
  45270. end;
  45271. result := true;
  45272. end;
  45273. function TSQLRecordProperties.FieldBitsFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldBits;
  45274. begin
  45275. if not FieldBitsFromRawUTF8(aFields,Result) then
  45276. FillZero(result);
  45277. end;
  45278. function TSQLRecordProperties.CSVFromFieldBits(const Bits: TSQLFieldBits): RawUTF8;
  45279. var f: integer;
  45280. W: TTextWriter;
  45281. begin
  45282. W := TTextWriter.CreateOwnedStream(512);
  45283. try
  45284. for f := 0 to Fields.Count-1 do
  45285. if f in Bits then begin
  45286. W.AddString(Fields.List[f].Name);
  45287. W.Add(',');
  45288. end;
  45289. W.CancelLastComma;
  45290. W.SetText(result);
  45291. finally
  45292. W.Free;
  45293. end;
  45294. end;
  45295. function TSQLRecordProperties.FieldIndexDynArrayFromRawUTF8(
  45296. const aFields: array of RawUTF8; var Indexes: TSQLFieldIndexDynArray): boolean;
  45297. var f,ndx: integer;
  45298. begin
  45299. result := false;
  45300. if self=nil then
  45301. exit;
  45302. for f := 0 to high(aFields) do begin
  45303. ndx := Fields.IndexByName(aFields[f]);
  45304. if ndx<0 then
  45305. exit; // invalid field name
  45306. AddFieldIndex(Indexes,ndx);
  45307. end;
  45308. result := true;
  45309. end;
  45310. function TSQLRecordProperties.FieldIndexDynArrayFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldIndexDynArray;
  45311. begin
  45312. if not FieldIndexDynArrayFromRawUTF8(aFields,result) then
  45313. result := nil;
  45314. end;
  45315. function TSQLRecordProperties.FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8;
  45316. var Indexes: TSQLFieldIndexDynArray): boolean;
  45317. var ndx: integer;
  45318. P: PUTF8Char;
  45319. FieldName: ShortString;
  45320. begin
  45321. result := false;
  45322. if self=nil then
  45323. exit;
  45324. P := pointer(aFieldsCSV);
  45325. while P<>nil do begin
  45326. GetNextItemShortString(P,FieldName);
  45327. FieldName[ord(FieldName[0])+1] := #0; // make PUTF8Char
  45328. ndx := Fields.IndexByName(@FieldName[1]);
  45329. if ndx<0 then
  45330. exit; // invalid field name
  45331. AddFieldIndex(Indexes,ndx);
  45332. end;
  45333. result := true;
  45334. end;
  45335. function TSQLRecordProperties.FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldIndexDynArray;
  45336. begin
  45337. if not FieldIndexDynArrayFromCSV(aFieldsCSV,result) then
  45338. result := nil;
  45339. end;
  45340. function TSQLRecordProperties.FieldIndexDynArrayFromBlobField(aBlobField: PPropInfo;
  45341. var Indexes: TSQLFieldIndexDynArray): boolean;
  45342. var f: integer;
  45343. begin
  45344. if self<>nil then
  45345. for f := 0 to high(BlobFields) do
  45346. if BlobFields[f].fPropInfo=aBlobField then begin
  45347. AddFieldIndex(Indexes,BlobFields[f].PropertyIndex);
  45348. result := true;
  45349. exit;
  45350. end;
  45351. result := false;
  45352. end;
  45353. function TSQLRecordProperties.AppendFieldName(FieldIndex: Integer;
  45354. var Text: RawUTF8; ForceNoRowID: boolean): boolean;
  45355. begin
  45356. result := false; // success
  45357. if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN then
  45358. if ForceNoRowID then
  45359. Text := Text+'ID' else
  45360. Text := Text+'RowID' else
  45361. if (self=nil) or (cardinal(FieldIndex)>=cardinal(Fields.Count)) then
  45362. result := true else
  45363. Text := Text+Fields.List[FieldIndex].Name;
  45364. end;
  45365. function TSQLRecordProperties.MainFieldName(ReturnFirstIfNoUnique: boolean=false): RawUTF8;
  45366. begin
  45367. if (self=nil) or (Table=nil) or (MainField[ReturnFirstIfNoUnique]<0) then
  45368. result := '' else
  45369. result := Fields.List[MainField[ReturnFirstIfNoUnique]].Name;
  45370. end;
  45371. procedure TSQLRecordProperties.RegisterCustomFixedSizeRecordProperty(
  45372. aTable: TClass; aRecordSize: cardinal; const aName: RawUTF8;
  45373. aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes;
  45374. aFieldWidth: integer; aData2Text: TOnSQLPropInfoRecord2Text;
  45375. aText2Data: TOnSQLPropInfoRecord2Data);
  45376. begin
  45377. Fields.Add(TSQLPropInfoRecordFixedSize.Create(aRecordSize,aName,Fields.Count,
  45378. aPropertyPointer,aAttributes,aFieldWidth,aData2Text,aText2Data));
  45379. end;
  45380. procedure TSQLRecordProperties.RegisterCustomRTTIRecordProperty(aTable: TClass;
  45381. aRecordInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer;
  45382. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0;
  45383. aData2Text: TOnSQLPropInfoRecord2Text=nil;
  45384. aText2Data: TOnSQLPropInfoRecord2Data=nil);
  45385. begin
  45386. Fields.Add(TSQLPropInfoRecordRTTI.Create(aRecordInfo,aName,Fields.Count,
  45387. aPropertyPointer,aAttributes,aFieldWidth,aData2Text,aText2Data));
  45388. end;
  45389. procedure TSQLRecordProperties.RegisterCustomPropertyFromRTTI(aTable: TClass;
  45390. aTypeInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer;
  45391. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0);
  45392. begin
  45393. Fields.Add(TSQLPropInfoCustomJSON.Create(aTypeInfo,aName,Fields.Count,
  45394. aPropertyPointer,aAttributes,aFieldWidth));
  45395. end;
  45396. procedure TSQLRecordProperties.RegisterCustomPropertyFromTypeName(aTable: TClass;
  45397. const aTypeName, aName: RawUTF8; aPropertyPointer: pointer;
  45398. aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0);
  45399. begin
  45400. Fields.Add(TSQLPropInfoCustomJSON.Create(aTypeName,aName,Fields.Count,
  45401. aPropertyPointer,aAttributes,aFieldWidth));
  45402. end;
  45403. { TSynValidateUniqueField }
  45404. function TSynValidateUniqueField.Process(aFieldIndex: integer; const Value: RawUTF8;
  45405. var ErrorMsg: string): boolean;
  45406. var aID: TID;
  45407. begin
  45408. result := false;
  45409. if Value='' then
  45410. ErrorMsg := sValidationFieldVoid else
  45411. if (fProcessRest=nil) or (fProcessRec=nil) then
  45412. result := true else
  45413. with fProcessRec.RecordProps do
  45414. if cardinal(aFieldIndex)>=cardinal(Fields.Count) then
  45415. result := true else begin
  45416. SetID(fProcessRest.OneFieldValue(Table,'RowID',
  45417. Fields.List[aFieldIndex].Name+'=:('+QuotedStr(Value,'''')+'):'),aID);
  45418. if (aID>0) and (aID<>fProcessRec.fID) then
  45419. ErrorMsg := sValidationFieldDuplicate else
  45420. result := true;
  45421. end;
  45422. end;
  45423. { TSynValidateUniqueFields }
  45424. procedure TSynValidateUniqueFields.SetParameters(const Value: RawUTF8);
  45425. var V: TPUtf8CharDynArray;
  45426. tmp: TSynTempBuffer;
  45427. begin
  45428. tmp.Init(Value);
  45429. try
  45430. JSONDecode(tmp.buf,['FieldNames'],V,True);
  45431. CSVToRawUTF8DynArray(V[0],fFieldNames);
  45432. finally
  45433. tmp.Done;
  45434. end;
  45435. end;
  45436. function TSynValidateUniqueFields.Process(aFieldIndex: integer;
  45437. const Value: RawUTF8; var ErrorMsg: string): boolean;
  45438. var where: RawUTF8;
  45439. i: integer;
  45440. aID: TID;
  45441. begin
  45442. where := ''; // alf: to circumvent FPC issues
  45443. if (fProcessRest=nil) or (fProcessRec=nil) or (fFieldNames=nil) then
  45444. result := true else begin
  45445. for i := 0 to high(fFieldNames) do begin
  45446. if where<>'' then
  45447. where := where+' AND ';
  45448. where := where+fFieldNames[i]+'=:('+
  45449. QuotedStr(fProcessRec.GetFieldValue(fFieldNames[i]),'''')+'):';
  45450. end;
  45451. SetID(fProcessRest.OneFieldValue(fProcessRec.RecordClass,'ID',where),aID);
  45452. if (aID>0) and (aID<>fProcessRec.fID) then begin
  45453. ErrorMsg := sValidationFieldDuplicate;
  45454. result := false;
  45455. end else
  45456. result := true;
  45457. end;
  45458. end;
  45459. { TJSONSerializer }
  45460. procedure TJSONSerializer.WriteObject(Value: TObject; Options: TTextWriterWriteObjectOptions);
  45461. var Added: boolean;
  45462. CustomComment: RawUTF8;
  45463. procedure HR(P: PPropInfo=nil);
  45464. begin
  45465. if woHumanReadable in Options then begin
  45466. if CustomComment<>'' then begin
  45467. AddShort(' // ');
  45468. AddString(CustomComment);
  45469. CustomComment := '';
  45470. end;
  45471. AddCRAndIndent;
  45472. end;
  45473. if P=nil then
  45474. exit;
  45475. AddPropName(P^.Name); // would handle twoForceJSONExtended in CustomOptions
  45476. if woHumanReadable in Options then
  45477. Add(' ');
  45478. Added := true;
  45479. end;
  45480. var P: PPropInfo;
  45481. i, V, c, codepage: integer;
  45482. V64: Int64;
  45483. Obj: TObject;
  45484. List: TList absolute Value;
  45485. {$ifndef LVCL}
  45486. Coll: TCollection absolute Value;
  45487. {$endif}
  45488. Str: TStrings absolute Value;
  45489. Utf: TRawUTF8List absolute Value;
  45490. Table: TSQLTable absolute Value;
  45491. aClassType: TClass;
  45492. Kind: TTypeKind;
  45493. UtfP: PPUtf8CharArray;
  45494. IsObj: TJSONObject;
  45495. IsObjCustomIndex: integer;
  45496. PS: PShortString;
  45497. WS: WideString;
  45498. {$ifdef HASVARUSTRING}
  45499. US: UnicodeString;
  45500. {$endif}
  45501. tmp: RawByteString;
  45502. dyn: TDynArray;
  45503. dynObjArray: PClassInstance;
  45504. {$ifndef NOVARIANTS}
  45505. VVariant: variant;
  45506. {$endif}
  45507. label next;
  45508. begin
  45509. if not (woHumanReadable in Options) or (fHumanReadableLevel<0) then
  45510. fHumanReadableLevel := 0;
  45511. if (self=nil) or (Value=nil) then begin
  45512. AddShort('null'); // return void object
  45513. exit;
  45514. end;
  45515. aClassType := PClass(Value)^;
  45516. IsObj := JSONObject(aClassType,IsObjCustomIndex,[cpWrite]);
  45517. if woFullExpand in Options then
  45518. if IsObj=oSynMonitor then begin // nested values do not need extended info
  45519. exclude(Options,woFullExpand);
  45520. include(Options,woEnumSetsAsText); // only needed info is textual enums
  45521. end else begin
  45522. Add('{');
  45523. AddInstanceName(Value,':');
  45524. end;
  45525. case IsObj of
  45526. // handle custom class serialization
  45527. oCustom:
  45528. with JSONCustomParsers[IsObjCustomIndex] do begin
  45529. if Assigned(Writer) then
  45530. Writer(self,Value,Options);
  45531. exit;
  45532. end;
  45533. // handle JSON arrays
  45534. oSQLTable:
  45535. Table.GetJSONValues(Stream,true);
  45536. oList, oObjectList, {$ifndef LVCL}oCollection,{$endif} oUtfs, oStrings: begin
  45537. HR;
  45538. Add('['); // write as JSON array of JSON objects
  45539. inc(fHumanReadableLevel);
  45540. case IsObj of
  45541. oList: // TList
  45542. for c := 0 to List.Count-1 do begin
  45543. WriteObject(List.List[c],Options);
  45544. Add(',');
  45545. end;
  45546. oObjectList: begin
  45547. if not (woObjectListWontStoreClassName in Options) then
  45548. // TObjectList will include "ClassName":"TMyObject" field
  45549. Options := Options+[woStoreClassName];
  45550. for c := 0 to List.Count-1 do begin
  45551. WriteObject(List.List[c],Options);
  45552. Add(',');
  45553. end;
  45554. end;
  45555. {$ifndef LVCL}
  45556. oCollection:
  45557. for c := 0 to Coll.Count-1 do begin
  45558. WriteObject(Coll.Items[c],Options);
  45559. Add(',');
  45560. end;
  45561. {$endif}
  45562. oUtfs: begin
  45563. UtfP := Utf.ListPtr;
  45564. for c := 0 to Utf.Count-1 do begin
  45565. HR;
  45566. Add('"');
  45567. AddJSONEscape(UtfP^[c]);
  45568. Add('"',',');
  45569. end;
  45570. end;
  45571. oStrings:
  45572. for c := 0 to Str.Count-1 do begin
  45573. HR;
  45574. Add('"');
  45575. AddJSONEscapeString(Str[c]);
  45576. Add('"',',');
  45577. end;
  45578. end;
  45579. CancelLastComma;
  45580. dec(fHumanReadableLevel);
  45581. HR;
  45582. Add(']');
  45583. if woFullExpand in Options then
  45584. Add('}');
  45585. exit;
  45586. end;
  45587. end;
  45588. // handle JSON object
  45589. Add('{');
  45590. inc(fHumanReadableLevel);
  45591. if woStoreClassName in Options then begin // optional "ClassName":"TObjectClass"
  45592. HR;
  45593. AddPropName('ClassName');
  45594. Add('"');
  45595. AddShort(PShortString(PPointer(PPtrInt(Value)^+vmtClassName)^)^);
  45596. Add('"',',');
  45597. end;
  45598. if IsObj in [oSQLRecord,oSQLMany] then begin
  45599. // manual handling of TSQLRecord.ID property serialization
  45600. HR;
  45601. AddPropName('ID');
  45602. if woHumanReadable in Options then
  45603. Add(' ');
  45604. Add(TSQLRecord(Value).fID);
  45605. Add(',');
  45606. end else begin
  45607. if woStorePointer in Options then begin // "Address":"0431298a" field
  45608. HR;
  45609. AddPropName('Address');
  45610. Add('"');
  45611. AddPointer(PtrUInt(Value));
  45612. Add('"',',');
  45613. end;
  45614. case IsObj of
  45615. oException: begin
  45616. HR;
  45617. AddPropName('Message');
  45618. Add('"');
  45619. AddJSONEscapeString(Exception(Value).Message);
  45620. Add('"',',');
  45621. end;
  45622. end;
  45623. end;
  45624. repeat
  45625. for i := 1 to InternalClassPropInfo(aClassType,P) do begin
  45626. if Assigned(OnWriteObject) and OnWriteObject(self,Value,P,Options) then
  45627. goto next else
  45628. if IsObj in [oSQLRecord,oSQLMany] then begin // ignore "stored AS_UNIQUE"
  45629. if IsRowIDShort(P^.Name) then
  45630. goto next; // should not happen
  45631. end else
  45632. if (not (woStoreStoredFalse in Options)) and
  45633. (not P^.IsStored(Value)) then
  45634. goto next; // ignore regular "stored false" attribute
  45635. Added := false; // HR(P) would write field name and set Added := true
  45636. Kind := P^.PropType^.Kind;
  45637. case Kind of
  45638. tkInt64{$ifdef FPC}, tkQWord{$endif}: begin
  45639. V64 := P^.GetInt64Prop(Value);
  45640. if not ((woDontStoreDefault in Options) and (V64=Int64(P^.Default))) then begin
  45641. HR(P);
  45642. if (woTimeLogAsText in Options) and (P^.PropType^.GetSQLFieldType
  45643. in [sftTimeLog,sftModTime,sftCreateTime]) then begin
  45644. Add('"');
  45645. AddTimeLog(@V64);
  45646. Add('"');
  45647. end else
  45648. Add(V64);
  45649. end;
  45650. end;
  45651. {$ifdef FPC} tkBool, {$endif}
  45652. tkEnumeration, tkInteger, tkSet: begin
  45653. V := P^.GetOrdProp(Value);
  45654. if (V<>P^.Default) or not (woDontStoreDefault in Options) then begin
  45655. HR(P);
  45656. if {$ifdef FPC}(Kind=tkBool) or{$endif}
  45657. ((Kind=tkEnumeration) and (P^.TypeInfo=TypeInfo(boolean))) then
  45658. Add(boolean(V)) else
  45659. if (woFullExpand in Options) or (woHumanReadable in Options) or
  45660. (woEnumSetsAsText in Options) or
  45661. (twoEnumSetsAsTextInRecord in CustomOptions) then
  45662. case Kind of
  45663. tkEnumeration:
  45664. with P^.PropType^.EnumBaseType^ do begin
  45665. Add('"');
  45666. PS := GetEnumNameOrd(V);
  45667. if twoTrimLeftEnumSets in CustomOptions then
  45668. AddTrimLeftLowerCase(PS) else
  45669. AddShort(PS^);
  45670. Add('"');
  45671. if woHumanReadableEnumSetAsComment in Options then
  45672. GetEnumNameAll(CustomComment,'',true);
  45673. end;
  45674. tkSet:
  45675. with P^.PropType^.SetEnumType^ do begin
  45676. GetSetNameCSV(self,V,',',woHumanReadableFullSetsAsStar in Options);
  45677. if woHumanReadableEnumSetAsComment in Options then
  45678. GetEnumNameAll(CustomComment,'"*" or a set of ',true);
  45679. end;
  45680. else
  45681. Add(V);
  45682. end else
  45683. Add(V); // typecast enums and sets as plain integer by default
  45684. end;
  45685. end;
  45686. {$ifdef FPC}tkAString,{$endif} tkLString: begin
  45687. codepage := P^.PropType^.AnsiStringCodePage;
  45688. if (codepage=CP_SQLRAWBLOB) and not (woSQLRawBlobAsBase64 in Options) then begin
  45689. if not (woDontStoreEmptyString in Options) then begin
  45690. HR(P);
  45691. AddShort('""');
  45692. end;
  45693. end else begin
  45694. P^.GetLongStrProp(Value,tmp);
  45695. if (tmp<>'') or not (woDontStoreEmptyString in Options) then begin
  45696. HR(P);
  45697. Add('"');
  45698. if (IsObj=oPersistentPassword) and (codepage=CP_UTF8) and
  45699. ((woHideSynPersistentPassword in Options) or
  45700. (woFullExpand in Options)) and
  45701. P^.GetterIsField and (P^.GetterAddr(Value)=
  45702. TSynPersistentWithPassword(Value).GetPasswordFieldAddress) then begin
  45703. if tmp<>'' then
  45704. AddShort('***')
  45705. end else
  45706. AddAnyAnsiString(tmp,twJSONEscape,codepage);
  45707. Add('"');
  45708. end;
  45709. end;
  45710. end;
  45711. tkFloat: begin
  45712. HR(P);
  45713. if (P^.TypeInfo=TypeInfo(Currency)) and P^.GetterIsField then
  45714. AddCurr64(PInt64(P^.GetterAddr(Value))^) else
  45715. if P^.TypeInfo=TypeInfo(TDateTime) then begin
  45716. if woDateTimeWithMagic in Options then
  45717. AddNoJSONEscape(@JSON_SQLDATE_MAGIC_QUOTE_VAR,4) else
  45718. Add('"');
  45719. AddDateTime(P^.GetDoubleProp(Value));
  45720. if woDateTimeWithZSuffix in Options then
  45721. Add('Z');
  45722. Add('"');
  45723. end else
  45724. Add(P^.GetFloatProp(Value),DOUBLE_PRECISION);
  45725. end;
  45726. {$ifdef HASVARUSTRING}
  45727. tkUString: begin // write converted to UTF-8
  45728. US := P^.GetUnicodeStrProp(Value);
  45729. if (US<>'') or not (woDontStoreEmptyString in Options) then begin
  45730. HR(P);
  45731. Add('"');
  45732. AddJSONEscapeW(pointer(US));
  45733. Add('"');
  45734. end;
  45735. end;
  45736. {$endif}
  45737. tkWString: begin // write converted to UTF-8
  45738. P^.GetWideStrProp(Value,WS);
  45739. if (WS<>'') or not (woDontStoreEmptyString in Options) then begin
  45740. HR(P);
  45741. Add('"');
  45742. AddJSONEscapeW(pointer(WS));
  45743. Add('"');
  45744. end;
  45745. end;
  45746. tkDynArray: begin
  45747. HR(P);
  45748. P^.GetDynArray(Value,dyn);
  45749. dynObjArray := P^.DynArrayIsObjArrayInstance;
  45750. if dynObjArray<>nil then begin
  45751. if dyn.Count=0 then begin
  45752. if woHumanReadableEnumSetAsComment in Options then
  45753. CustomComment := FormatUTF8('array of {%}',[
  45754. ClassFieldNamesAllPropsAsText(dynObjArray^.ItemClass,true)]);
  45755. Add('[',']');
  45756. end else begin // do not use AddDynArrayJSON to support HR
  45757. inc(fHumanReadableLevel);
  45758. Add('[');
  45759. for c := 0 to dyn.Count-1 do begin
  45760. WriteObject(PPointerArray(dyn.Value^)^[c],Options);
  45761. Add(',');
  45762. end;
  45763. CancelLastComma;
  45764. dec(fHumanReadableLevel);
  45765. HR;
  45766. Add(']');
  45767. end;
  45768. end else
  45769. AddDynArrayJSON(dyn);
  45770. end;
  45771. {$ifdef PUBLISHRECORD}
  45772. tkRecord{$ifdef FPC},tkObject{$endif}: begin
  45773. HR(P);
  45774. AddRecordJSON(P^.GetFieldAddr(Value)^,P^.PropType^);
  45775. end;
  45776. {$endif}
  45777. {$ifndef NOVARIANTS}
  45778. tkVariant: begin // stored as JSON, e.g. '1.234' or '"text"'
  45779. HR(P);
  45780. P^.GetVariantProp(Value,VVariant);
  45781. AddVariant(VVariant,twJSONEscape);
  45782. end;
  45783. {$endif}
  45784. tkClass: begin
  45785. Obj := P^.GetObjProp(Value);
  45786. case IsObj of
  45787. oSQLRecord,oSQLMany: // TSQLRecord or inherited
  45788. if PropIsIDTypeCastedField(P,IsObj,Value) then begin
  45789. HR(P);
  45790. Add(PtrInt(Obj)); // not true instances, but ID
  45791. end else
  45792. if Obj<>nil then begin
  45793. HR(P);
  45794. WriteObject(Obj,Options);
  45795. end;
  45796. else // TPersistent or any class defined with $M+
  45797. if Obj<>nil then begin
  45798. HR(P);
  45799. WriteObject(Obj,Options);
  45800. end;
  45801. end;
  45802. end;
  45803. // tkString (shortstring) and tkInterface is not handled
  45804. end;
  45805. if Added then
  45806. Add(',');
  45807. next: P := P^.Next;
  45808. end;
  45809. if woDontStoreInherited in Options then
  45810. break;
  45811. aClassType := aClassType.ClassParent;
  45812. until aClassType=nil;
  45813. CancelLastComma;
  45814. dec(fHumanReadableLevel);
  45815. HR;
  45816. Add('}');
  45817. if woFullExpand in Options then
  45818. Add('}');
  45819. end;
  45820. procedure TJSONSerializer.AddInstancePointer(Instance: TObject; SepChar: AnsiChar;
  45821. IncludeUnitName: boolean);
  45822. var info: PTypeInfo;
  45823. begin
  45824. if IncludeUnitName then begin
  45825. info := PPointer(PPtrInt(Instance)^+vmtTypeInfo)^;
  45826. if info<>nil then begin // avoid GPF if not RTTI for this class
  45827. with info^ do
  45828. AddShort(PClassType(AlignToPtr(@Name[ord(Name[0])+1]))^.UnitName);
  45829. Add('.');
  45830. end;
  45831. end;
  45832. inherited AddInstancePointer(Instance,SepChar,IncludeUnitName);
  45833. end;
  45834. procedure TJSONSerializer.SetSQLRecordOptions(Value: TJSONSerializerSQLRecordOptions);
  45835. begin
  45836. fSQLRecordOptions := Value;
  45837. if Value*[jwoAsJsonNotAsString,jwoID_str]<>[] then
  45838. if (ColNames<>nil) and (ColNames[0]='"RowID":') then
  45839. ColNames[0] := '"ID":'; // as expected by AJAX
  45840. end;
  45841. { TSQLVirtualTableModule }
  45842. constructor TSQLVirtualTableModule.Create(aTableClass: TSQLVirtualTableClass;
  45843. aServer: TSQLRestServer);
  45844. begin
  45845. fTableClass := aTableClass;
  45846. fServer := aServer;
  45847. fTableClass.GetTableModuleProperties(fFeatures);
  45848. fModuleName := fTableClass.ModuleName;
  45849. if fFeatures.FileExtension='' then // default extension is the module name
  45850. fFeatures.FileExtension := UTF8ToString(LowerCase(fModuleName));
  45851. end;
  45852. function TSQLVirtualTableModule.FileName(const aTableName: RawUTF8): TFileName;
  45853. begin
  45854. result := UTF8ToString(aTableName)+'.'+FileExtension;;
  45855. if fFilePath='' then
  45856. result := ExeVersion.ProgramFilePath+result else
  45857. result := IncludeTrailingPathDelimiter(fFilePath)+result;
  45858. end;
  45859. { TSQLVirtualTable }
  45860. constructor TSQLVirtualTable.Create(aModule: TSQLVirtualTableModule;
  45861. const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray);
  45862. var aClass: TSQLRestStorageClass;
  45863. aServer: TSQLRestServer;
  45864. begin
  45865. if (aModule=nil) or (aTableName='') then
  45866. raise EModelException.CreateUTF8('Invalid %.Create(%,"%")',[self,aModule,aTableName]);
  45867. fModule := aModule;
  45868. fTableName := aTableName;
  45869. if fModule.fFeatures.StaticClass<>nil then begin
  45870. // create new fStatic instance e.g. for TSQLVirtualTableLog
  45871. aServer := fModule.Server;
  45872. if aServer=nil then
  45873. raise EModelException.CreateUTF8('%.Server=nil for %.Create',[Module,self]) else
  45874. fStaticTableIndex := aServer.Model.GetTableIndex(aTableName);
  45875. if fStaticTableIndex>=0 then begin
  45876. fStaticTable := aServer.Model.Tables[fStaticTableIndex];
  45877. aClass := fModule.fFeatures.StaticClass;
  45878. if aClass.InheritsFrom(TSQLRestStorageInMemory) then
  45879. fStatic := TSQLRestStorageInMemoryClass(aClass).Create(fStaticTable,
  45880. fModule.Server,fModule.FileName(aTableName),
  45881. self.InheritsFrom(TSQLVirtualTableBinary)) else
  45882. fStatic := aClass.Create(fStaticTable,fModule.Server);
  45883. if length(aServer.fStaticVirtualTable)<>length(aServer.Model.Tables) then
  45884. SetLength(aServer.fStaticVirtualTable,length(aServer.Model.Tables));
  45885. aServer.fStaticVirtualTable[fStaticTableIndex] := fStatic;
  45886. if fStatic.InheritsFrom(TSQLRestStorage) then
  45887. fStaticStorage := TSQLRestStorage(fStatic);
  45888. end;
  45889. end;
  45890. end;
  45891. destructor TSQLVirtualTable.Destroy;
  45892. var aTableIndex: cardinal;
  45893. begin
  45894. if fStatic<>nil then begin
  45895. if (Module<>nil) and (Module.Server<>nil) then
  45896. with Module.Server do begin // temporary release (e.g. backup)
  45897. aTableIndex := Model.GetTableIndex(TableName);
  45898. if aTableIndex<cardinal(length(fStaticVirtualTable)) then begin
  45899. fStaticVirtualTable[aTableIndex] := nil;
  45900. if IsZero(fStaticVirtualTable,length(fStaticVirtualTable)*sizeof(pointer)) then
  45901. fStaticVirtualTable := nil;
  45902. end;
  45903. end;
  45904. fStatic.Free;
  45905. end;
  45906. inherited;
  45907. end;
  45908. function TSQLVirtualTable.Prepare(var Prepared: TSQLVirtualTablePrepared): boolean;
  45909. begin
  45910. result := Self<>nil;
  45911. if result then
  45912. if (vtWhereIDPrepared in fModule.Features) and
  45913. Prepared.IsWhereIDEquals(true) then
  45914. with Prepared.Where[0] do begin // check ID=?
  45915. Value.VType := ftNull; // mark TSQLVirtualTableCursorJSON expects it
  45916. OmitCheck := true;
  45917. Prepared.EstimatedCost := costPrimaryIndex;
  45918. Prepared.EstimatedRows := 1;
  45919. end else begin
  45920. Prepared.EstimatedCost := costFullScan;
  45921. Prepared.EstimatedRows := 1000000;
  45922. end;
  45923. end;
  45924. function TSQLVirtualTable.Drop: boolean;
  45925. begin
  45926. result := false; // no DROP TABLE to be implemented here
  45927. end;
  45928. function TSQLVirtualTable.Delete(aRowID: Int64): boolean;
  45929. begin
  45930. result := false; // no DELETE to be implemented here
  45931. end;
  45932. function TSQLVirtualTable.Insert(aRowID: Int64;
  45933. var Values: TSQLVarDynArray; out insertedRowID: Int64): boolean;
  45934. begin
  45935. result := false; // no INSERT to be implemented here
  45936. end;
  45937. function TSQLVirtualTable.Update(oldRowID, newRowID: Int64;
  45938. var Values: TSQLVarDynArray): boolean;
  45939. begin
  45940. result := false; // no UPDATE to be implemented here
  45941. end;
  45942. function TSQLVirtualTable.Transaction(aState: TSQLVirtualTableTransaction;
  45943. aSavePoint: integer): boolean;
  45944. begin
  45945. result := (Module<>nil) and (vtWrite in Module.Features) and
  45946. (aState in [vttBegin, vttSync, vttCommit, vttSavePoint, vttRelease]);
  45947. end;
  45948. function TSQLVirtualTable.Rename(const NewName: RawUTF8): boolean;
  45949. begin
  45950. result := false;
  45951. end;
  45952. class function TSQLVirtualTable.ModuleName: RawUTF8;
  45953. const LEN: array[-1..2] of byte = (1,16,11,4);
  45954. begin
  45955. if self=nil then
  45956. result := '' else begin
  45957. result := RawUTF8(ClassName);
  45958. system.delete(result,1,LEN[IdemPCharArray(pointer(result),
  45959. ['TSQLVIRTUALTABLE','TSQLVIRTUAL','TSQL'])]);
  45960. end;
  45961. end;
  45962. class function TSQLVirtualTable.StructureFromClass(aClass: TSQLRecordClass;
  45963. const aTableName: RawUTF8): RawUTF8;
  45964. begin
  45965. result := FormatUTF8('CREATE TABLE % (%',[aTableName,
  45966. GetVirtualTableSQLCreate(aClass.RecordProps)]);
  45967. end;
  45968. function TSQLVirtualTable.Structure: RawUTF8;
  45969. begin
  45970. result := '';
  45971. if Self<>nil then
  45972. if (Static<>nil) then
  45973. // e.g. for TSQLVirtualTableJSON or TSQLVirtualTableExternal
  45974. Result := StructureFromClass(StaticTable,TableName) else
  45975. if (Module<>nil) and (Module.RecordClass<>nil) then
  45976. // e.g. for TSQLVirtualTableLog
  45977. Result := StructureFromClass(Module.RecordClass,TableName);
  45978. end;
  45979. { TSQLVirtualTableCursor }
  45980. constructor TSQLVirtualTableCursor.Create(aTable: TSQLVirtualTable);
  45981. begin
  45982. fTable := aTable;
  45983. end;
  45984. procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar; aValue: Int64);
  45985. begin
  45986. aResult.VType := ftInt64;
  45987. aResult.VInt64 := aValue;
  45988. end;
  45989. procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar; const aValue: double);
  45990. begin
  45991. aResult.VType := ftDouble;
  45992. aResult.VDouble := aValue;
  45993. end;
  45994. procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar; const aValue: RawUTF8);
  45995. begin
  45996. aResult.VType := ftUTF8;
  45997. fColumnTemp := aValue; // temporary copy available until next Column() call
  45998. aResult.VText := pointer(fColumnTemp);
  45999. end;
  46000. procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar;
  46001. aValue: PUTF8Char; aValueLength: integer);
  46002. begin
  46003. aResult.VType := ftUTF8;
  46004. SetString(fColumnTemp,PAnsiChar(aValue),aValueLength); // temporary copy
  46005. aResult.VText := pointer(fColumnTemp);
  46006. end;
  46007. procedure TSQLVirtualTableCursor.SetColumnBlob(var aResult: TSQLVar;
  46008. aValue: pointer; aValueLength: integer);
  46009. begin
  46010. aResult.VType := ftBlob;
  46011. SetString(fColumnTemp,PAnsiChar(aValue),aValueLength); // temporary copy
  46012. aResult.VBlob := pointer(fColumnTemp);
  46013. aResult.VBlobLen := aValueLength;
  46014. end;
  46015. { TSQLLog }
  46016. procedure TSQLLog.CreateLogWriter;
  46017. begin
  46018. fWriterClass := TJSONSerializer;
  46019. inherited CreateLogWriter;
  46020. end;
  46021. { TSQLVirtualTableCursorIndex }
  46022. function TSQLVirtualTableCursorIndex.HasData: boolean;
  46023. begin
  46024. result := (self<>nil) and (fCurrent<=fMax);
  46025. end;
  46026. function TSQLVirtualTableCursorIndex.Next: boolean;
  46027. begin
  46028. if self=nil then
  46029. result := false else begin
  46030. if fCurrent<=fMax then
  46031. inc(fCurrent);
  46032. result := true;
  46033. end;
  46034. end;
  46035. function TSQLVirtualTableCursorIndex.Search(
  46036. const Prepared: TSQLVirtualTablePrepared): boolean;
  46037. begin
  46038. fCurrent := 0; // mark EOF by default
  46039. fMax := -1;
  46040. result := self<>nil;
  46041. end;
  46042. { TSQLVirtualTablePrepared }
  46043. function TSQLVirtualTablePrepared.IsWhereIDEquals(CalledFromPrepare: Boolean): boolean;
  46044. begin
  46045. result := (WhereCount=1) and (Where[0].Column=VIRTUAL_TABLE_ROWID_COLUMN) and
  46046. (CalledFromPrepare or (Where[0].Value.VType=ftInt64)) and
  46047. (Where[0].Operation=soEqualTo);
  46048. end;
  46049. function TSQLVirtualTablePrepared.IsWhereOneFieldEquals: boolean;
  46050. begin
  46051. result := (WhereCount=1) and (Where[0].Column>=0) and
  46052. (Where[0].Operation=soEqualTo);
  46053. end;
  46054. { TSQLVirtualTableJSON }
  46055. constructor TSQLVirtualTableJSON.Create(aModule: TSQLVirtualTableModule;
  46056. const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray);
  46057. begin
  46058. inherited Create(aModule,aTableName,FieldCount,Fields);
  46059. fStaticInMemory := fStatic as TSQLRestStorageInMemory;
  46060. end;
  46061. function TSQLVirtualTableJSON.Delete(aRowID: Int64): boolean;
  46062. begin
  46063. result := (Static<>nil) and Static.Delete(StaticTable,aRowID);
  46064. if result and (StaticStorage<>nil) and (StaticStorage.Owner<>nil) then
  46065. StaticStorage.Owner.fCache.NotifyDeletion(StaticTable,aRowID);
  46066. end;
  46067. function TSQLVirtualTableJSON.Drop: boolean;
  46068. begin
  46069. if (self<>nil) and (Static<>nil) then begin
  46070. fStaticInMemory.RollBack(0); // close any pending transaction
  46071. fStaticInMemory.fValue.Clear;
  46072. fStaticInMemory.Modified := true; // force update file after clear
  46073. fStaticInMemory.UpdateFile;
  46074. result := true;
  46075. end else
  46076. result := false;
  46077. end;
  46078. class procedure TSQLVirtualTableJSON.GetTableModuleProperties(
  46079. var aProperties: TVirtualTableModuleProperties);
  46080. begin
  46081. aProperties.Features := [vtWrite,vtWhereIDPrepared];
  46082. aProperties.CursorClass := TSQLVirtualTableCursorJSON;
  46083. aProperties.StaticClass := TSQLRestStorageInMemoryExternal; // will flush Cache
  46084. if InheritsFrom(TSQLVirtualTableBinary) then
  46085. aProperties.FileExtension := 'data';
  46086. // default will follow the class name, e.g. '.json' for TSQLVirtualTableJSON
  46087. end;
  46088. function TSQLVirtualTableJSON.Insert(aRowID: Int64;
  46089. var Values: TSQLVarDynArray; out insertedRowID: Int64): boolean;
  46090. var aRecord: TSQLRecord;
  46091. begin
  46092. result := false;
  46093. if (self=nil) or (Static=nil) then
  46094. exit;
  46095. aRecord := StaticTable.Create;
  46096. try
  46097. if aRecord.SetFieldSQLVars(Values) then begin
  46098. if aRowID>0 then
  46099. aRecord.fID := aRowID;
  46100. insertedRowID := fStaticInMemory.AddOne(aRecord,aRowID>0,
  46101. aRecord.GetJSONValues(true,False,soInsert));
  46102. if insertedRowID>0 then begin
  46103. if fStaticInMemory.Owner<>nil then
  46104. fStaticInMemory.Owner.fCache.Notify(aRecord,soInsert);
  46105. result := true;
  46106. end;
  46107. end;
  46108. finally
  46109. if not result then
  46110. aRecord.Free; // on success, aRecord will stay in Values[]
  46111. end;
  46112. end;
  46113. function TSQLVirtualTableJSON.Prepare(var Prepared: TSQLVirtualTablePrepared): boolean;
  46114. begin
  46115. result := inherited Prepare(Prepared); // optimize ID=? WHERE clause
  46116. if result and (Static<>nil) then begin
  46117. if Prepared.IsWhereOneFieldEquals then
  46118. with Prepared.Where[0] do
  46119. if fStaticInMemory.UniqueFieldHash(Column)<>nil then begin
  46120. Value.VType := ftNull; // mark TSQLVirtualTableCursorJSON expects it
  46121. OmitCheck := true;
  46122. Prepared.EstimatedCost := costSecondaryIndex;
  46123. Prepared.EstimatedRows := 10;
  46124. end else
  46125. if Prepared.EstimatedCost in [costFullScan,costScanWhere] then
  46126. Prepared.EstimatedRows := fStaticInMemory.Count;
  46127. if fStaticInMemory.fIDSorted and (Prepared.OrderByCount=1) then
  46128. // ascending IDs ?
  46129. with Prepared.OrderBy[0] do
  46130. if (Column=VIRTUAL_TABLE_ROWID_COLUMN) and not Desc then
  46131. Prepared.OmitOrderBy := true;
  46132. end;
  46133. end;
  46134. function TSQLVirtualTableJSON.Update(oldRowID, newRowID: Int64;
  46135. var Values: TSQLVarDynArray): boolean;
  46136. var i: integer;
  46137. begin
  46138. result := false;
  46139. if (self=nil) or (Static=nil) or
  46140. (oldRowID<>newRowID) or (newRowID<=0) then // don't allow ID change
  46141. exit;
  46142. if fStaticInMemory.UpdateOne(newRowID,Values) then begin
  46143. if (fStaticInMemory.Owner<>nil) then begin
  46144. i := fStaticInMemory.IDToIndex(newRowID);
  46145. if i>=0 then
  46146. fStaticInMemory.Owner.fCache.Notify(
  46147. TSQLRecord(fStaticInMemory.fValue.List[i]),soUpdate);
  46148. end;
  46149. result := true;
  46150. end;
  46151. end;
  46152. { TSQLVirtualTableCursorJSON }
  46153. function TSQLVirtualTableCursorJSON.Column(aColumn: integer;
  46154. var aResult: TSQLVar): boolean;
  46155. var Value: TObjectList;
  46156. begin
  46157. if (self=nil) or (fCurrent>fMax) or
  46158. (TSQLVirtualTableJSON(Table).Static=nil) then begin
  46159. result := false;
  46160. exit;
  46161. end;
  46162. Value := TSQLVirtualTableJSON(Table).fStaticInMemory.fValue;
  46163. if Cardinal(fCurrent)>=Cardinal(Value.Count) then
  46164. result := False else begin
  46165. if aColumn=VIRTUAL_TABLE_ROWID_COLUMN then begin
  46166. aResult.VType := ftInt64;
  46167. aResult.VInt64 := TSQLRecord(Value.List[fCurrent]).fID;
  46168. end else
  46169. with TSQLVirtualTableJSON(Table).fStaticInMemory.fStoredClassRecordProps.Fields do
  46170. if cardinal(aColumn)>=cardinal(Count) then
  46171. aResult.VType := ftNull else
  46172. List[aColumn].GetFieldSQLVar(Value.List[fCurrent],aResult,fColumnTemp);
  46173. result := true;
  46174. end;
  46175. end;
  46176. function TSQLVirtualTableCursorJSON.Search(const Prepared: TSQLVirtualTablePrepared): boolean;
  46177. var Hash: TListFieldHash;
  46178. begin
  46179. result := inherited Search(Prepared); // mark EOF by default
  46180. if (not result) or (not Table.InheritsFrom(TSQLVirtualTableJSON)) or
  46181. (TSQLVirtualTableJSON(Table).fStaticInMemory=nil) then
  46182. result := false else
  46183. with TSQLVirtualTableJSON(Table).fStaticInMemory do begin
  46184. if Count>0 then
  46185. // if something to search in
  46186. if Prepared.IsWhereIDEquals(false) then begin // ID=?
  46187. fMax := IDToIndex(Prepared.Where[0].Value.VInt64); // binary search
  46188. if fMax>=0 then
  46189. fCurrent := fMax; // ID found
  46190. end else
  46191. if Prepared.IsWhereOneFieldEquals then
  46192. with Prepared.Where[0] do begin
  46193. Hash := UniqueFieldHash(Column);
  46194. if Hash<>nil then begin // optimized hash-based search
  46195. fStoredClassRecordProps.Fields.List[Column].SetFieldSQLVar(fSearchRec,Value);
  46196. fMax := Hash.Find(fSearchRec);
  46197. if fMax>=0 then
  46198. fCurrent := fMax; // value found with O(1) search
  46199. end else
  46200. fMax := Count-1; // loop all records in ID order
  46201. end else
  46202. fMax := Count-1; // loop all records in ID order
  46203. result := true; // no DB error
  46204. end;
  46205. end;
  46206. { TSQLVirtualTableLog }
  46207. type
  46208. /// Record associated to Virtual Table implemented in Delphi, for Read/Only
  46209. // access to a .log file, as created by TSynLog
  46210. // - not used as real instances, but only used by the TSQLVirtualTableLog module
  46211. // to provide the field layout needed to create the column layout for the
  46212. // CREATE TABLE statement
  46213. TSQLRecordLogFile = class(TSQLRecordVirtualTableAutoID)
  46214. protected
  46215. fContent: RawUTF8;
  46216. fDateTime: TDateTime;
  46217. fLevel: TSynLogInfo;
  46218. published
  46219. /// the log event time stamp
  46220. property DateTime: TDateTime read fDateTime;
  46221. /// the log event level
  46222. property Level: TSynLogInfo read fLevel;
  46223. /// the textual message associated to the log event
  46224. property Content: RawUTF8 read fContent;
  46225. end;
  46226. constructor TSQLVirtualTableLog.Create(aModule: TSQLVirtualTableModule;
  46227. const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray);
  46228. var aFileName: TFileName;
  46229. begin
  46230. inherited Create(aModule,aTableName,Fieldcount,Fields);
  46231. if (FieldCount=1) then
  46232. aFileName := UTF8ToString(Fields[0]) else
  46233. aFileName := aModule.FileName(aTableName);
  46234. fLogFile := TSynLogFile.Create(aFileName);
  46235. end;
  46236. destructor TSQLVirtualTableLog.Destroy;
  46237. begin
  46238. fLogFile.Free;
  46239. inherited;
  46240. end;
  46241. class procedure TSQLVirtualTableLog.GetTableModuleProperties(
  46242. var aProperties: TVirtualTableModuleProperties);
  46243. begin
  46244. aProperties.Features := [vtWhereIDPrepared];
  46245. aProperties.CursorClass := TSQLVirtualTableCursorLog;
  46246. aProperties.RecordClass := TSQLRecordLogFile;
  46247. end;
  46248. { TSQLVirtualTableCursorLog }
  46249. function TSQLVirtualTableCursorLog.Column(aColumn: integer;
  46250. var aResult: TSQLVar): boolean;
  46251. var LogFile: TSynLogFile;
  46252. begin
  46253. result := false;
  46254. if (self=nil) or (fCurrent>fMax) then
  46255. exit;
  46256. LogFile := TSQLVirtualTableLog(Table).fLogFile;
  46257. if LogFile=nil then
  46258. exit;
  46259. case aColumn of
  46260. -1: SetColumn(aResult,fCurrent+1); // ID = row index + 1
  46261. 0: SetColumn(aResult,LogFile.EventDateTime(fCurrent));
  46262. 1: SetColumn(aResult,ord(LogFile.EventLevel[fCurrent]));
  46263. 2: SetColumn(aResult,LogFile.LinePointers[fCurrent],LogFile.LineSize(fCurrent));
  46264. else exit;
  46265. end;
  46266. result := true;
  46267. end;
  46268. function TSQLVirtualTableCursorLog.Search(
  46269. const Prepared: TSQLVirtualTablePrepared): boolean;
  46270. begin
  46271. result := inherited Search(Prepared); // mark EOF by default
  46272. if result then begin
  46273. fMax := TSQLVirtualTableLog(Table).fLogFile.Count-1; // search all range
  46274. if Prepared.IsWhereIDEquals(false) then begin
  46275. fCurrent := Prepared.Where[0].Value.VInt64-1; // ID=? -> index := ID-1
  46276. if cardinal(fCurrent)<=cardinal(fMax) then
  46277. fMax := fCurrent else // found one
  46278. fMax := fCurrent-1; // out of range ID
  46279. end;
  46280. end;
  46281. end;
  46282. { TAuthSession }
  46283. procedure TAuthSession.ComputeProtectedValues;
  46284. begin // here User.GroupRights and fPrivateKey should have been set
  46285. fLastAccess64 := GetTickCount64;
  46286. fTimeOutMS := User.GroupRights.SessionTimeout*(1000*60); // min to ms
  46287. fAccessRights := User.GroupRights.SQLAccessRights;
  46288. fPrivateSalt := fID+'+'+fPrivateKey;
  46289. fPrivateSaltHash :=
  46290. crc32(crc32(0,pointer(fPrivateSalt),length(fPrivateSalt)),
  46291. pointer(User.PasswordHashHexa),length(User.PasswordHashHexa));
  46292. fRemoteIP := FindIniNameValue(pointer(fSentHeaders),'REMOTEIP: ');
  46293. end;
  46294. constructor TAuthSession.Create(aCtxt: TSQLRestServerURIContext; aUser: TSQLAuthUser);
  46295. var GID: TSQLAuthGroup;
  46296. begin
  46297. fUser := aUser;
  46298. if (aCtxt<>nil) and (User<>nil) and (User.fID<>0) then begin
  46299. GID := User.GroupRights; // save pseudo TSQLAuthGroup = ID
  46300. User.GroupRights := aCtxt.Server.fSQLAuthGroupClass.Create(aCtxt.Server,GID);
  46301. if User.GroupRights.fID<>0 then begin
  46302. // compute the next Session ID
  46303. with aCtxt.Server do begin
  46304. if fSessionCounter>=cardinal(maxInt) then
  46305. fSessionCounter := 10 else
  46306. if fSessionCounter=75 then // avoid IDCardinal=0 (77) or 1 (76)
  46307. fSessionCounter := 78 else
  46308. inc(fSessionCounter);
  46309. fIDCardinal := fSessionCounter xor 77;
  46310. UInt32ToUtf8(fIDCardinal,fID);
  46311. end;
  46312. // set session parameters
  46313. fPrivateKey := SHA256(NowToString+fID);
  46314. aCtxt.Server.RetrieveBlob(aCtxt.Server.fSQLAuthUserClass,User.fID,'Data',User.fData);
  46315. if (aCtxt.Call<>nil) and (aCtxt.Call.InHead<>'') then
  46316. fSentHeaders := aCtxt.Call.InHead;
  46317. ComputeProtectedValues;
  46318. {$ifdef WITHLOG}
  46319. aCtxt.Log.Log(sllUserAuth,
  46320. 'New "%" session %/% created at %/% running %',
  46321. [User.GroupRights.Ident,User.LogonName,fIDCardinal,fRemoteIP,
  46322. aCtxt.Call^.LowLevelConnectionID,aCtxt.UserAgent],self);
  46323. {$endif}
  46324. exit; // create successfull
  46325. end;
  46326. // on error: set GroupRights back to a pseudo TSQLAuthGroup = ID
  46327. User.GroupRights.Free;
  46328. User.GroupRights := GID;
  46329. end;
  46330. raise ESecurityException.CreateUTF8('Invalid %.Create(%,%)',[self,aCtxt,aUser]);
  46331. end;
  46332. destructor TAuthSession.Destroy;
  46333. begin
  46334. if User<>nil then begin
  46335. User.GroupRights.Free;
  46336. fUser.Free;
  46337. end;
  46338. ObjArrayClear(fMethods);
  46339. ObjArrayClear(fInterfaces);
  46340. inherited;
  46341. end;
  46342. function TAuthSession.GetUserName: RawUTF8;
  46343. begin
  46344. if User=nil then
  46345. result := '' else
  46346. result := User.LogonName;
  46347. end;
  46348. function TAuthSession.GetUserID: TID;
  46349. begin
  46350. if User=nil then
  46351. result := 0 else
  46352. result := User.fID;
  46353. end;
  46354. function TAuthSession.GetGroupID: TID;
  46355. begin
  46356. if User=nil then
  46357. result := 0 else
  46358. result := User.GroupRights.ID;
  46359. end;
  46360. const TAUTHSESSION_MAGIC = 1;
  46361. procedure TAuthSession.SaveTo(W: TFileBufferWriter);
  46362. begin
  46363. W.Write1(TAUTHSESSION_MAGIC);
  46364. W.WriteVarUInt32(IDCardinal);
  46365. W.WriteVarUInt32(fUser.fID);
  46366. fUser.GetBinaryValues(W); // User.fGroup is a pointer, but would be overriden
  46367. W.WriteVarUInt32(fUser.GroupRights.fID);
  46368. fUser.GroupRights.GetBinaryValues(W);
  46369. W.Write(fPrivateKey);
  46370. W.Write(fSentHeaders);
  46371. end; // TODO: persist ORM/SOA stats? -> rather integrate them before saving
  46372. constructor TAuthSession.CreateFrom(var P: PAnsiChar; Server: TSQLRestServer);
  46373. var PB: PByte absolute P;
  46374. begin
  46375. if PB^=TAUTHSESSION_MAGIC then
  46376. inc(PB) else
  46377. raise ESynException.CreateUTF8('%.CreateFrom() with invalid format',[self]);
  46378. fIDCardinal := FromVarUInt32(PB);
  46379. UInt32ToUtf8(fIDCardinal,fID);
  46380. fUser := Server.SQLAuthUserClass.Create;
  46381. fUser.fID := FromVarUInt32(PB);
  46382. fUser.SetBinaryValues(P); // fUser.fGroup would be overriden by true instance
  46383. fUser.fGroup := Server.SQLAuthGroupClass.Create;
  46384. fUser.fGroup.fID := FromVarUInt32(PB);
  46385. fUser.fGroup.SetBinaryValues(P);
  46386. fPrivateKey := FromVarString(PB);
  46387. fSentHeaders := FromVarString(PB);
  46388. ComputeProtectedValues;
  46389. end;
  46390. { TSQLAccessRights }
  46391. procedure TSQLAccessRights.Edit(aTableIndex: integer; C, R, U, D: Boolean);
  46392. begin
  46393. if C then
  46394. Include(POST,aTableIndex) else
  46395. Exclude(POST,aTableindex);
  46396. if R then
  46397. Include(GET,aTableIndex) else
  46398. Exclude(GET,aTableindex);
  46399. if U then
  46400. Include(PUT,aTableIndex) else
  46401. Exclude(PUT,aTableindex);
  46402. if D then
  46403. Include(DELETE,aTableIndex) else
  46404. Exclude(DELETE,aTableindex);
  46405. end;
  46406. procedure TSQLAccessRights.Edit(aTableIndex: integer; aRights: TSQLOccasions);
  46407. begin
  46408. if soInsert in aRights then
  46409. Include(POST,aTableIndex) else
  46410. Exclude(POST,aTableindex);
  46411. if soSelect in aRights then
  46412. Include(GET,aTableIndex) else
  46413. Exclude(GET,aTableindex);
  46414. if soUpdate in aRights then
  46415. Include(PUT,aTableIndex) else
  46416. Exclude(PUT,aTableindex);
  46417. if soDelete in aRights then
  46418. Include(DELETE,aTableIndex) else
  46419. Exclude(DELETE,aTableindex);
  46420. end;
  46421. procedure TSQLAccessRights.Edit(aModel: TSQLModel; aTable: TSQLRecordClass;
  46422. C, R, U, D: Boolean);
  46423. begin
  46424. Edit(aModel.GetTableIndexExisting(aTable),C,R,U,D);
  46425. end;
  46426. procedure TSQLAccessRights.Edit(aModel: TSQLModel; aTable: TSQLRecordClass;
  46427. aRights: TSQLOccasions);
  46428. begin
  46429. Edit(aModel.GetTableIndexExisting(aTable),aRights);
  46430. end;
  46431. procedure TSQLAccessRights.FromString(P: PUTF8Char);
  46432. begin
  46433. FillcharFast(self,sizeof(self),0);
  46434. if P=nil then
  46435. exit;
  46436. AllowRemoteExecute := TSQLAllowRemoteExecute(byte(GetNextItemCardinal(P)));
  46437. SetBitCSV(GET,MAX_SQLTABLES,P);
  46438. SetBitCSV(POST,MAX_SQLTABLES,P);
  46439. SetBitCSV(PUT,MAX_SQLTABLES,P);
  46440. SetBitCSV(DELETE,MAX_SQLTABLES,P);
  46441. end;
  46442. function TSQLAccessRights.ToString: RawUTF8;
  46443. begin
  46444. result := FormatUTF8('%,%,%,%,%',
  46445. [Byte(AllowRemoteExecute),
  46446. GetBitCSV(GET,MAX_SQLTABLES), GetBitCSV(POST,MAX_SQLTABLES),
  46447. GetBitCSV(PUT,MAX_SQLTABLES), GetBitCSV(DELETE,MAX_SQLTABLES)]);
  46448. end;
  46449. function TSQLAccessRights.CanExecuteORMWrite(Method: TSQLURIMethod;
  46450. Table: TSQLRecordClass; TableIndex: integer; const TableID: TID;
  46451. Context: TSQLRestServerURIContext): boolean;
  46452. begin
  46453. result := true;
  46454. case Method of
  46455. mPOST: // POST=ADD=INSERT
  46456. if Table<>nil then // ExecuteORMWrite will check reSQL access right
  46457. result := (TableIndex in POST);
  46458. mPUT: // PUT=UPDATE
  46459. result := (Table<>nil) and
  46460. ((TableIndex in PUT) or
  46461. ((TableID>0) and (Context.Session>CONST_AUTHENTICATION_NOT_USED) and
  46462. (Table=Context.Server.fSQLAuthUserClass) and (TableID=Context.SessionUser) and
  46463. (reUserCanChangeOwnPassword in AllowRemoteExecute)));
  46464. mDelete:
  46465. result := (Table<>nil) and (TableIndex in DELETE) and
  46466. ((TableID>0) or (reUrlEncodedDelete in AllowRemoteExecute));
  46467. end;
  46468. end;
  46469. { TSQLAuthGroup }
  46470. function TSQLAuthGroup.GetSQLAccessRights: TSQLAccessRights;
  46471. begin
  46472. if self=nil then
  46473. FillcharFast(result,sizeof(result),0) else
  46474. result.FromString(pointer(AccessRights));
  46475. end;
  46476. class procedure TSQLAuthGroup.InitializeTable(Server: TSQLRestServer;
  46477. const FieldName: RawUTF8; Options: TSQLInitializeTableOptions);
  46478. var G: TSQLAuthGroup;
  46479. A: TSQLAccessRights;
  46480. U: TSQLAuthUser;
  46481. AuthUserIndex, AuthGroupIndex: integer;
  46482. AdminID, SupervisorID, UserID: PtrInt;
  46483. begin
  46484. inherited; // will create any needed index
  46485. if (Server<>nil) and (FieldName='') then
  46486. if Server.HandleAuthentication then begin
  46487. // create default Groups and Users (we are already in a Transaction)
  46488. AuthGroupIndex := Server.Model.GetTableIndexExisting(Server.fSQLAuthUserClass);
  46489. AuthUserIndex := Server.Model.GetTableIndexExisting(Server.fSQLAuthGroupClass);
  46490. if not (itoNoAutoCreateGroups in Options) then begin
  46491. G := Server.fSQLAuthGroupClass.Create;
  46492. try
  46493. // POSTSQL SELECTSQL Service AuthR AuthW TablesR TablesW
  46494. // Admin Yes Yes Yes Yes Yes Yes Yes
  46495. // Supervisor No Yes Yes Yes No Yes Yes
  46496. // User No No Yes No No Yes Yes
  46497. // Guest No No No No No Yes No
  46498. A := FULL_ACCESS_RIGHTS;
  46499. G.Ident := 'Admin';
  46500. G.SQLAccessRights := A;
  46501. G.SessionTimeout := 10;
  46502. AdminID := Server.Add(G,true);
  46503. G.Ident := 'Supervisor';
  46504. A.AllowRemoteExecute := SUPERVISOR_ACCESS_RIGHTS.AllowRemoteExecute;
  46505. A.Edit(AuthUserIndex,[soSelect]); // AuthUser R/O
  46506. A.Edit(AuthGroupIndex,[soSelect]); // AuthGroup R/O
  46507. G.SQLAccessRights := A;
  46508. G.SessionTimeout := 60;
  46509. SupervisorID := Server.Add(G,true);
  46510. G.Ident := 'User';
  46511. Exclude(A.AllowRemoteExecute,reSQLSelectWithoutTable);
  46512. Exclude(A.GET,AuthUserIndex); // no Auth R
  46513. Exclude(A.GET,AuthGroupIndex);
  46514. G.SQLAccessRights := A;
  46515. G.SessionTimeout := 60;
  46516. UserID := Server.Add(G,true);
  46517. G.Ident := 'Guest';
  46518. A.AllowRemoteExecute := [];
  46519. FillcharFast(A.POST,sizeof(TSQLFieldTables),0); // R/O access
  46520. FillcharFast(A.PUT,sizeof(TSQLFieldTables),0);
  46521. FillcharFast(A.DELETE,sizeof(TSQLFieldTables),0);
  46522. G.SQLAccessRights := A;
  46523. G.SessionTimeout := 60;
  46524. Server.Add(G,true);
  46525. finally
  46526. G.Free;
  46527. end;
  46528. if (not (itoNoAutoCreateUsers in Options)) and
  46529. (Server.TableRowCount(Server.fSQLAuthUserClass)=0) then begin
  46530. U := Server.fSQLAuthUserClass.Create;
  46531. try
  46532. U.LogonName := 'Admin';
  46533. U.PasswordHashHexa := AuthAdminDefaultPassword;
  46534. U.DisplayName := U.LogonName;
  46535. U.GroupRights := TSQLAuthGroup(AdminID);
  46536. Server.Add(U,true);
  46537. U.LogonName := 'Supervisor';
  46538. U.PasswordHashHexa := AuthSupervisorDefaultPassword;
  46539. U.DisplayName := U.LogonName;
  46540. U.GroupRights := TSQLAuthGroup(SupervisorID);
  46541. Server.Add(U,true);
  46542. U.LogonName := 'User';
  46543. U.PasswordHashHexa := AuthUserDefaultPassword;
  46544. U.DisplayName := U.LogonName;
  46545. U.GroupRights := TSQLAuthGroup(UserID);
  46546. Server.Add(U,true);
  46547. finally
  46548. U.Free;
  46549. end;
  46550. end;
  46551. end;
  46552. end;
  46553. end;
  46554. procedure TSQLAuthGroup.SetSQLAccessRights(const Value: TSQLAccessRights);
  46555. begin
  46556. if self<>nil then
  46557. AccessRights := Value.ToString;
  46558. end;
  46559. { TSQLAuthUser }
  46560. class function TSQLAuthUser.ComputeHashedPassword(
  46561. const aPasswordPlain, aHashSalt: RawUTF8; aHashRound: integer): RawUTF8;
  46562. const TSQLAUTHUSER_SALT = 'salt';
  46563. var dig: TSHA256Digest;
  46564. begin
  46565. if aHashSalt='' then
  46566. result := SHA256(TSQLAUTHUSER_SALT+aPasswordPlain) else begin
  46567. PBKDF2_HMAC_SHA256(aPasswordPlain,aHashSalt,aHashRound,dig);
  46568. result := SHA256DigestToString(dig);
  46569. FillcharFast(dig,sizeof(dig),0);
  46570. end;
  46571. end;
  46572. procedure TSQLAuthUser.SetPasswordPlain(const Value: RawUTF8);
  46573. begin
  46574. if self<>nil then
  46575. PasswordHashHexa := ComputeHashedPassword(Value);
  46576. end;
  46577. procedure TSQLAuthUser.SetPassword(const aPasswordPlain, aHashSalt: RawUTF8;
  46578. aHashRound: integer);
  46579. begin
  46580. if self<>nil then
  46581. PasswordHashHexa := ComputeHashedPassword(aPasswordPlain,aHashSalt,aHashRound);
  46582. end;
  46583. function TSQLAuthUser.CanUserLog(Ctxt: TSQLRestServerURIContext): boolean;
  46584. begin
  46585. result := true; // any existing TSQLAuthUser is allowed by default
  46586. end;
  46587. { TSQLRestServerAuthentication }
  46588. constructor TSQLRestServerAuthentication.Create(aServer: TSQLRestServer);
  46589. begin
  46590. fServer := aServer;
  46591. fOptions := [saoUserByLogonOrID];
  46592. end;
  46593. function TSQLRestServerAuthentication.AuthSessionRelease(
  46594. Ctxt: TSQLRestServerURIContext): boolean;
  46595. var aUserName: RawUTF8;
  46596. aSessionID: cardinal;
  46597. i: integer;
  46598. begin
  46599. result := false;
  46600. if fServer.fSessions=nil then
  46601. exit;
  46602. aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  46603. if aUserName='' then
  46604. exit;
  46605. aSessionID := Ctxt.InputIntOrVoid['Session'];
  46606. if aSessionID=0 then
  46607. aSessionID := Ctxt.InputHexaOrVoid['SessionHex'];
  46608. if aSessionID=0 then
  46609. exit;
  46610. result := true; // recognized GET ModelRoot/auth?UserName=...&Session=...
  46611. // allow only to delete its own session - ticket [7723fa7ebd]
  46612. if aSessionID=Ctxt.Session then
  46613. for i := 0 to fServer.fSessions.Count-1 do
  46614. with TAuthSession(fServer.fSessions.List[i]) do
  46615. if (fIDCardinal=aSessionID) and (fUser.LogonName=aUserName) then begin
  46616. fServer.SessionDelete(i,Ctxt);
  46617. Ctxt.Success;
  46618. break;
  46619. end;
  46620. end;
  46621. function TSQLRestServerAuthentication.GetUser(Ctxt: TSQLRestServerURIContext;
  46622. const aUserName: RawUTF8): TSQLAuthUser;
  46623. var UserID: TID;
  46624. err: integer;
  46625. begin
  46626. UserID := GetInt64(pointer(aUserName),err);
  46627. if (err<>0) or (UserID<=0) or not (saoUserByLogonOrID in fOptions) then
  46628. UserID := 0;
  46629. if Assigned(fServer.OnAuthenticationUserRetrieve) then
  46630. result := fServer.OnAuthenticationUserRetrieve(self,Ctxt,UserID,aUserName) else begin
  46631. if UserID<>0 then begin // try if TSQLAuthUser.ID was transmitted
  46632. result := fServer.fSQLAuthUserClass.Create(fServer,UserID); // may use ORM cache :)
  46633. if result.fID=0 then
  46634. FreeAndNil(result);
  46635. end else
  46636. result := nil;
  46637. if result=nil then
  46638. result := fServer.fSQLAuthUserClass.Create(fServer,'LogonName=?',[aUserName]);
  46639. if (result.fID=0) and
  46640. (saoHandleUnknownLogonAsStar in fOptions) then
  46641. if fServer.Retrieve('LogonName=?',[],['*'],result) then begin
  46642. result.LogonName := aUserName;
  46643. result.DisplayName := aUserName;
  46644. end;
  46645. end;
  46646. if (result=nil) or (result.fID=0) then begin
  46647. fServer.InternalLog('%.LogonName=% not found',[fServer.fSQLAuthUserClass,aUserName],sllUserAuth);
  46648. FreeAndNil(result);
  46649. end else
  46650. if not result.CanUserLog(Ctxt) then begin
  46651. fServer.InternalLog('%.CanUserLog(%) returned FALSE -> rejected',[result,aUserName],sllUserAuth);
  46652. FreeAndNil(result);
  46653. end;
  46654. end;
  46655. procedure TSQLRestServerAuthentication.SessionCreate(Ctxt: TSQLRestServerURIContext;
  46656. var User: TSQLAuthUser);
  46657. var Session: TAuthSession;
  46658. begin
  46659. if User<>nil then
  46660. try // now client is authenticated -> create a session
  46661. fServer.SessionCreate(User,Ctxt,Session); // call Ctxt.AuthenticationFailed on error
  46662. if Session<>nil then
  46663. with Session.User do
  46664. Ctxt.Returns(['result',Session.fPrivateSalt,'logonid',IDValue,
  46665. 'logonname',LogonName,'logondisplay',DisplayName,'logongroup',GroupRights.IDValue,
  46666. 'server',ExeVersion.ProgramName,'version',ExeVersion.Version.Detailed]);
  46667. finally
  46668. User.Free;
  46669. end;
  46670. end;
  46671. class function TSQLRestServerAuthentication.ClientGetSessionKey(
  46672. Sender: TSQLRestClientURI; User: TSQLAuthUser; const aNameValueParameters: array of const): RawUTF8;
  46673. var resp: RawUTF8;
  46674. values: TPUtf8CharDynArray;
  46675. begin
  46676. if (Sender.CallBackGet('Auth',aNameValueParameters,resp)<>HTML_SUCCESS) or
  46677. (JSONDecode(pointer(resp),['result','data','server','version',
  46678. 'logonid','logonname','logondisplay','logongroup'],values)=nil) then begin
  46679. Sender.fSessionData := '';
  46680. result := '';
  46681. end else begin
  46682. SetString(result,values[0],StrLen(values[0]));
  46683. Base64ToBin(PAnsiChar(values[1]),StrLen(values[1]),Sender.fSessionData);
  46684. Sender.fSessionServer := values[2];
  46685. Sender.fSessionVersion := values[3];
  46686. SetID(values[4],User.fID);
  46687. User.LogonName := values[5]; // set/fix using values from server
  46688. User.DisplayName := values[6];
  46689. User.GroupRights := pointer(GetInteger(values[7]));
  46690. end;
  46691. end;
  46692. class function TSQLRestServerAuthentication.ClientSetUser(Sender: TSQLRestClientURI;
  46693. const aUserName, aPassword: RawUTF8; aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword;
  46694. const aHashSalt: RawUTF8; aHashRound: integer): boolean;
  46695. var U: TSQLAuthUser;
  46696. key: RawUTF8;
  46697. begin
  46698. result := false;
  46699. if Sender=nil then
  46700. exit;
  46701. try
  46702. Sender.SessionClose;
  46703. U := TSQLAuthUser.Create;
  46704. try
  46705. U.LogonName := trim(aUserName);
  46706. U.DisplayName := U.LogonName;
  46707. if aPasswordKind<>passClear then
  46708. U.PasswordHashHexa := aPassword else
  46709. if aHashSalt='' then
  46710. U.PasswordPlain := aPassword else // compute SHA256('salt'+aPassword)
  46711. U.SetPassword(aPassword,aHashSalt,aHashRound);
  46712. key := ClientComputeSessionKey(Sender,U);
  46713. result := Sender.SessionCreate(self,U,key);
  46714. finally
  46715. U.Free;
  46716. end;
  46717. finally
  46718. if Assigned(Sender.OnSetUser) then
  46719. Sender.OnSetUser(Sender); // always notify of user change, even if failed
  46720. end;
  46721. end;
  46722. { TSQLRestServerAuthenticationURI }
  46723. function TSQLRestServerAuthenticationURI.RetrieveSession(
  46724. Ctxt: TSQLRestServerURIContext): TAuthSession;
  46725. begin
  46726. result := nil;
  46727. if (Ctxt=nil) or (Ctxt.URISessionSignaturePos=0) then
  46728. exit;
  46729. // expected format is 'session_signature='Hexa8(SessionID)'...
  46730. if (Ctxt.URISessionSignaturePos>0) and
  46731. (Ctxt.URISessionSignaturePos+(18+8)<=length(Ctxt.Call^.Url)) and
  46732. HexDisplayToCardinal(PAnsiChar(pointer(Ctxt.Call^.url))+Ctxt.URISessionSignaturePos+18,Ctxt.Session) then
  46733. result := fServer.SessionAccess(Ctxt);
  46734. end;
  46735. class procedure TSQLRestServerAuthenticationURI.ClientSessionSign(
  46736. Sender: TSQLRestClientURI; var Call: TSQLRestURIParams);
  46737. begin
  46738. if (Sender<>nil) and (Sender.fSessionID<>0) and (Sender.fSessionUser<>nil) then
  46739. if PosEx(RawUTF8('?'),Call.url,1)=0 then
  46740. Call.url := Call.url+'?session_signature='+Sender.fSessionIDHexa8 else
  46741. Call.url := Call.url+'&session_signature='+Sender.fSessionIDHexa8;
  46742. end;
  46743. { TSQLRestServerAuthenticationSignedURI }
  46744. // expected format is session_signature=
  46745. // Hexa8(SessionID)+
  46746. // Hexa8(TimeStamp)+
  46747. // Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+
  46748. // Hexa8(TimeStamp)+url))
  46749. constructor TSQLRestServerAuthenticationSignedURI.Create(aServer: TSQLRestServer);
  46750. begin
  46751. inherited Create(aServer);
  46752. fTimeStampCoherencySeconds := 5;
  46753. end;
  46754. procedure TSQLRestServerAuthenticationSignedURI.SetNoTimeStampCoherencyCheck(value: boolean);
  46755. begin
  46756. if self<>nil then
  46757. fNoTimeStampCoherencyCheck := value;
  46758. end;
  46759. function TSQLRestServerAuthenticationSignedURI.RetrieveSession(
  46760. Ctxt: TSQLRestServerURIContext): TAuthSession;
  46761. var aTimeStamp, aSignature, aExpectedSignature: cardinal;
  46762. PTimeStamp: PAnsiChar;
  46763. aURLlength: Integer;
  46764. begin
  46765. result := inherited RetrieveSession(Ctxt);
  46766. if result=nil then
  46767. exit; // no valid session ID in session_signature
  46768. if Ctxt.URISessionSignaturePos+(18+8+8+8)>length(Ctxt.Call^.url) then begin
  46769. result := nil;
  46770. exit;
  46771. end;
  46772. aURLlength := Ctxt.URISessionSignaturePos-1;
  46773. PTimeStamp := @Ctxt.Call^.url[aURLLength+(20+8)]; // points to Hexa8(TimeStamp)
  46774. if HexDisplayToCardinal(PTimeStamp,aTimeStamp) and
  46775. (fNoTimeStampCoherencyCheck or (result.fLastTimeStamp=0) or
  46776. (aTimeStamp>=result.fLastTimeStamp-fTimeStampCoherencySeconds)) then begin
  46777. aExpectedSignature := crc32(crc32(result.fPrivateSaltHash,PTimeStamp,8),
  46778. pointer(Ctxt.Call^.url),aURLlength);
  46779. if HexDisplayToCardinal(PTimeStamp+8,aSignature) and
  46780. (aSignature=aExpectedSignature) then begin
  46781. if aTimeStamp>result.fLastTimeStamp then
  46782. result.fLastTimeStamp := aTimeStamp;
  46783. exit;
  46784. end else begin
  46785. {$ifdef WITHLOG}
  46786. Ctxt.Log.Log(sllUserAuth,'Invalid Signature: expected %, got %',
  46787. [Int64(aExpectedSignature),Int64(aSignature)],self);
  46788. {$endif}
  46789. end;
  46790. end else begin
  46791. {$ifdef WITHLOG}
  46792. Ctxt.Log.Log(sllUserAuth,'Invalid TimeStamp: expected >=%, got %',
  46793. [result.fLastTimeStamp-fTimeStampCoherencySeconds,aTimeStamp],self);
  46794. {$endif}
  46795. end;
  46796. result := nil; // indicates invalid signature
  46797. end;
  46798. class procedure TSQLRestServerAuthenticationSignedURI.ClientSessionSign(
  46799. Sender: TSQLRestClientURI; var Call: TSQLRestURIParams);
  46800. var nonce, blankURI: RawUTF8;
  46801. begin
  46802. if (Sender=nil) or (Sender.fSessionID=0) or (Sender.fSessionUser=nil) then
  46803. exit;
  46804. blankURI := Call.Url;
  46805. if PosEx(RawUTF8('?'),Call.Url,1)=0 then
  46806. Call.url := Call.Url+'?session_signature=' else
  46807. Call.url := Call.Url+'&session_signature=';
  46808. with Sender do begin
  46809. fSessionLastTick64 := GetTickCount64;
  46810. nonce := CardinalToHex(fSessionLastTick64 shr 8); // 256 ms resolution
  46811. Call.url := Call.url+fSessionIDHexa8+nonce+CardinalToHex(
  46812. crc32(crc32(fSessionPrivateKey,Pointer(nonce),length(nonce)),
  46813. Pointer(blankURI),length(blankURI)));
  46814. end;
  46815. end;
  46816. { TSQLRestServerAuthenticationDefault }
  46817. function TSQLRestServerAuthenticationDefault.Auth(
  46818. Ctxt: TSQLRestServerURIContext): boolean;
  46819. var aUserName, aPassWord, aClientNonce: RawUTF8;
  46820. User: TSQLAuthUser;
  46821. begin
  46822. result := true;
  46823. if AuthSessionRelease(Ctxt) then
  46824. exit;
  46825. aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  46826. aPassWord := Ctxt.InputUTF8OrVoid['Password'];
  46827. aClientNonce := Ctxt.InputUTF8OrVoid['ClientNonce'];
  46828. if (aUserName<>'') and (length(aClientNonce)>32) then begin
  46829. // GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... -> handshaking
  46830. User := GetUser(Ctxt,aUserName);
  46831. if User<>nil then
  46832. try
  46833. // check if match TSQLRestClientURI.SetUser() algorithm
  46834. if CheckPassword(Ctxt,User,aClientNonce,aPassWord) then
  46835. SessionCreate(Ctxt,User) else // will call Ctxt.AuthenticationFailed on error
  46836. Ctxt.AuthenticationFailed(afInvalidPassword);
  46837. finally
  46838. User.Free;
  46839. end else
  46840. Ctxt.AuthenticationFailed(afUnknownUser);
  46841. end else
  46842. if aUserName<>'' then
  46843. // only UserName=... -> return hexadecimal nonce content valid for 5 minutes
  46844. Ctxt.Results([ServerNonce(false)]) else
  46845. // parameters does not match any expected layout -> try next authentication
  46846. result := false;
  46847. end;
  46848. function TSQLRestServerAuthenticationDefault.CheckPassword(Ctxt: TSQLRestServerURIContext;
  46849. User: TSQLAuthUser; const aClientNonce, aPassWord: RawUTF8): boolean;
  46850. var aSalt: RawUTF8;
  46851. begin
  46852. aSalt := aClientNonce+User.LogonName+User.PasswordHashHexa;
  46853. result := (aPassWord=SHA256(fServer.Model.Root+ServerNonce(false)+aSalt)) or
  46854. // if current nonce failed, tries with previous 5 minutes' nonce
  46855. (aPassWord=SHA256(fServer.Model.Root+ServerNonce(true)+aSalt));
  46856. end;
  46857. class function TSQLRestServerAuthenticationDefault.ClientComputeSessionKey(
  46858. Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8;
  46859. var aServerNonce, aClientNonce: RawUTF8;
  46860. rnd: TSHA256Digest;
  46861. begin
  46862. result := '';
  46863. if User.LogonName='' then
  46864. exit;
  46865. aServerNonce := Sender.CallBackGetResult('Auth',['UserName',User.LogonName]);
  46866. if aServerNonce='' then
  46867. exit;
  46868. TAESPRNG.Main.FillRandom(@rnd,SizeOf(rnd));
  46869. aClientNonce := SHA256DigestToString(rnd);
  46870. result := ClientGetSessionKey(Sender,User,['UserName',User.LogonName,'Password',
  46871. Sha256(Sender.Model.Root+aServerNonce+aClientNonce+User.LogonName+User.PasswordHashHexa),
  46872. 'ClientNonce',aClientNonce]);
  46873. end;
  46874. { TSQLRestServerAuthenticationNone }
  46875. function TSQLRestServerAuthenticationNone.Auth(Ctxt: TSQLRestServerURIContext): boolean;
  46876. var aUserName: RawUTF8;
  46877. U: TSQLAuthUser;
  46878. begin
  46879. aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  46880. if aUserName='' then begin
  46881. result := false; // let's try another TSQLRestServerAuthentication class
  46882. exit;
  46883. end;
  46884. result := true; // this kind of weak authentication avoid stronger ones
  46885. if AuthSessionRelease(Ctxt) then
  46886. exit;
  46887. U := GetUser(Ctxt,aUserName);
  46888. if U=nil then
  46889. Ctxt.AuthenticationFailed(afUnknownUser) else
  46890. SessionCreate(Ctxt,U); // call Ctxt.AuthenticationFailed on error
  46891. end;
  46892. class function TSQLRestServerAuthenticationNone.ClientComputeSessionKey(
  46893. Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8;
  46894. begin
  46895. result := ClientGetSessionKey(Sender,User,['UserName',User.LogonName]);
  46896. end;
  46897. { TSQLRestServerAuthenticationHttpAbstract }
  46898. const
  46899. COOKIE_SESSION = 'mORMot_session_signature';
  46900. class procedure TSQLRestServerAuthenticationHttpAbstract.ClientSessionSign(
  46901. Sender: TSQLRestClientURI; var Call: TSQLRestURIParams);
  46902. begin
  46903. if (Sender<>nil) and (Sender.fSessionID<>0) and (Sender.fSessionUser<>nil) then
  46904. Call.InHead := Trim(Call.InHead+ // session ID transmitted as HTTP cookie
  46905. (#13#10'Cookie: '+COOKIE_SESSION+'=')+Sender.fSessionIDHexa8);
  46906. end;
  46907. class function TSQLRestServerAuthenticationHttpAbstract.ClientSetUser(
  46908. Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8;
  46909. aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword;
  46910. const aHashSalt: RawUTF8; aHashRound: integer): boolean;
  46911. var res: RawUTF8;
  46912. U: TSQLAuthUser;
  46913. begin
  46914. result := false;
  46915. if (aUserName='') or (Sender=nil) then
  46916. exit;
  46917. if aPasswordKind<>passClear then
  46918. raise ESecurityException.CreateUTF8('%.ClientSetUser(%) expects passClear',
  46919. [self,Sender]);
  46920. Sender.SessionClose;
  46921. try // inherited ClientSetUser() won't fit with Auth() method below
  46922. ClientSetUserHttpOnly(Sender,aUserName,aPassword);
  46923. Sender.fSessionAuthentication := self; // to enable ClientSessionSign()
  46924. U := TSQLAuthUser.Create;
  46925. try
  46926. U.LogonName := trim(aUserName);
  46927. res := ClientGetSessionKey(Sender,U,[]);
  46928. if res<>'' then
  46929. result := Sender.SessionCreate(self,U,res);
  46930. finally
  46931. U.Free;
  46932. end;
  46933. finally
  46934. if not result then begin
  46935. // on error, reverse all values
  46936. Sender.fSessionAuthentication := nil;
  46937. Sender.fSessionHttpHeader := '';
  46938. end;
  46939. if Assigned(Sender.OnSetUser) then
  46940. Sender.OnSetUser(Sender); // always notify of user change, even if failed
  46941. end;
  46942. end;
  46943. function TSQLRestServerAuthenticationHttpAbstract.RetrieveSession(
  46944. Ctxt: TSQLRestServerURIContext): TAuthSession;
  46945. var cookie: RawUTF8;
  46946. begin
  46947. cookie := Ctxt.InCookie[COOKIE_SESSION];
  46948. if (length(cookie)=8) and HexDisplayToCardinal(pointer(cookie),Ctxt.Session) then
  46949. result := fServer.SessionAccess(Ctxt) else
  46950. result := nil;
  46951. end;
  46952. class procedure TSQLRestServerAuthenticationHttpAbstract.ClientSetUserHttpOnly(
  46953. Sender: TSQLRestClientURI; const aUserName, aPasswordClear: RawUTF8);
  46954. begin
  46955. Sender.fSessionHttpHeader := ComputeAuthenticateHeader(aUserName,aPasswordClear);
  46956. end;
  46957. { TSQLRestServerAuthenticationHttpBasic }
  46958. class function TSQLRestServerAuthenticationHttpBasic.GetUserPassFromInHead(
  46959. Ctxt: TSQLRestServerURIContext; out userPass,user,pass: RawUTF8): boolean;
  46960. begin
  46961. userPass := Ctxt.InHeader['Authorization'];
  46962. if IdemPChar(pointer(userPass),'BASIC ') then begin
  46963. delete(userPass,1,6);
  46964. Split(Base64ToBin(userPass),':',user,pass);
  46965. result := user<>'';
  46966. end else
  46967. result := false;
  46968. end;
  46969. function TSQLRestServerAuthenticationHttpBasic.RetrieveSession(
  46970. Ctxt: TSQLRestServerURIContext): TAuthSession;
  46971. var userPass,user,pass: RawUTF8;
  46972. begin
  46973. result := inherited RetrieveSession(Ctxt);
  46974. if result=nil then
  46975. exit; // not a valid 'Cookie: mORMot_session_signature=...' header
  46976. if GetUserPassFromInHead(Ctxt,userPass,user,pass) then begin
  46977. if (result.fExpectedHttpAuthentication<>'') and // fast validation
  46978. (result.fExpectedHttpAuthentication=userPass) then
  46979. exit; // already previously authenticated
  46980. if user=Result.User.LogonName then
  46981. with Ctxt.Server.SQLAuthUserClass.Create do
  46982. try
  46983. PasswordPlain := pass; // compute SHA-256 hash of the supplied password
  46984. if PasswordHashHexa=result.User.PasswordHashHexa then begin
  46985. // match -> store header in result (locked by fSessions.fSafe.Lock)
  46986. result.fExpectedHttpAuthentication := userPass;
  46987. exit;
  46988. end;
  46989. finally
  46990. Free;
  46991. end;
  46992. end;
  46993. result := nil; // identicates authentication error
  46994. end;
  46995. class function TSQLRestServerAuthenticationHttpBasic.ComputeAuthenticateHeader(
  46996. const aUserName,aPasswordClear: RawUTF8): RawUTF8;
  46997. begin
  46998. result := 'Authorization: Basic '+BinToBase64(aUsername+':'+aPasswordClear);
  46999. end;
  47000. function TSQLRestServerAuthenticationHttpBasic.CheckPassword(Ctxt: TSQLRestServerURIContext;
  47001. User: TSQLAuthUser; const aPassWord: RawUTF8): boolean;
  47002. var expectedPass: RawUTF8;
  47003. begin
  47004. expectedPass := User.PasswordHashHexa;
  47005. User.PasswordPlain := aPassWord; // override with SHA-256 hash from HTTP header
  47006. result := User.PasswordHashHexa=expectedPass;
  47007. end;
  47008. function TSQLRestServerAuthenticationHttpBasic.Auth(Ctxt: TSQLRestServerURIContext): boolean;
  47009. var userPass,user,pass: RawUTF8;
  47010. U: TSQLAuthUser;
  47011. Session: TAuthSession;
  47012. begin
  47013. if Ctxt.InputExists['UserName'] then begin
  47014. result := false; // allow other schemes to check this request
  47015. exit;
  47016. end;
  47017. result := true; // this authentication method is exclusive to any other
  47018. if GetUserPassFromInHead(Ctxt,userPass,user,pass) then begin
  47019. U := GetUser(Ctxt,user);
  47020. if U<>nil then
  47021. try
  47022. if CheckPassword(Ctxt,U,pass) then begin
  47023. fServer.SessionCreate(U,Ctxt,Session); // call Ctxt.AuthenticationFailed on error
  47024. if Session<>nil then begin
  47025. // see TSQLRestServerAuthenticationHttpAbstract.ClientSessionSign()
  47026. Ctxt.SetOutSetCookie((COOKIE_SESSION+'=')+CardinalToHex(Session.IDCardinal));
  47027. if (rsoRedirectForbiddenToAuth in fServer.Options) and (Ctxt.ClientKind=ckAjax) then
  47028. Ctxt.Redirect(fServer.Model.Root) else
  47029. with Session.User do
  47030. Ctxt.Returns(['result',Session.IDCardinal,'logonid',IDValue,
  47031. 'logonname',LogonName,'logondisplay',DisplayName,'logongroup',GroupRights.IDValue,
  47032. 'server',ExeVersion.ProgramName,'version',ExeVersion.Version.Detailed]);
  47033. exit; // success
  47034. end;
  47035. end else
  47036. Ctxt.AuthenticationFailed(afInvalidPassword);
  47037. finally
  47038. U.Free;
  47039. end else
  47040. Ctxt.AuthenticationFailed(afUnknownUser);
  47041. end else begin
  47042. Ctxt.Call.OutHead := 'WWW-Authenticate: Basic realm="mORMot Server"';;
  47043. Ctxt.Error('',HTML_UNAUTHORIZED); // will popup for credentials in browser
  47044. end;
  47045. end;
  47046. {$ifdef SSPIAUTH}
  47047. { TSQLRestServerAuthenticationSSPI }
  47048. const
  47049. /// maximum number of Windows Authentication context to be handled at once
  47050. // - 64 should be big enough
  47051. MAXSSPIAUTHCONTEXTS = 64;
  47052. function TSQLRestServerAuthenticationSSPI.Auth(
  47053. Ctxt: TSQLRestServerURIContext): boolean;
  47054. var i: integer;
  47055. UserName, InDataEnc: RawUTF8;
  47056. ticks,ConnectionID: Int64;
  47057. BrowserAuth: Boolean;
  47058. CtxArr: TDynArray;
  47059. SecCtxIdx: Integer;
  47060. OutData: RawByteString;
  47061. User: TSQLAuthUser;
  47062. Session: TAuthSession;
  47063. begin
  47064. result := AuthSessionRelease(Ctxt);
  47065. if result or (not Ctxt.InputExists['UserName']) or (not Ctxt.InputExists['Data']) then
  47066. exit;
  47067. // use ConnectionID to find authentication session
  47068. ConnectionID := Ctxt.Call^.LowLevelConnectionID;
  47069. // GET ModelRoot/auth?UserName=&data=... -> windows SSPI auth
  47070. InDataEnc := Ctxt.InputUTF8['Data'];
  47071. if InDataEnc='' then begin
  47072. // client is browser and used HTTP headers to send auth data
  47073. InDataEnc := FindIniNameValue(pointer(Ctxt.Call.InHead),SECPKGNAMEHTTPAUTHORIZATION);
  47074. if InDataEnc = '' then begin
  47075. // no auth data sent, reply with supported auth methods
  47076. Ctxt.Call.OutHead := SECPKGNAMEHTTPWWWAUTHENTICATE;
  47077. Ctxt.Call.OutStatus := HTML_UNAUTHORIZED;
  47078. StatusCodeToErrorMsg(Ctxt.Call.OutStatus, Ctxt.Call.OutBody);
  47079. exit;
  47080. end;
  47081. BrowserAuth := True;
  47082. end else
  47083. BrowserAuth := False;
  47084. CtxArr.InitSpecific(TypeInfo(TSecContextDynArray),fSSPIAuthContexts,djInt64);
  47085. // check for outdated auth context
  47086. ticks := GetTickCount64-30000;
  47087. for i := High(fSSPIAuthContexts) downto 0 do
  47088. if ticks>fSSPIAuthContexts[i].CreatedTick64 then begin
  47089. FreeSecContext(fSSPIAuthContexts[i]);
  47090. CtxArr.Delete(i);
  47091. end;
  47092. // if no auth context specified, create a new one
  47093. result := true;
  47094. SecCtxIdx := CtxArr.Find(ConnectionID);
  47095. if SecCtxIdx<0 then begin
  47096. // 1st call: create SecCtxId
  47097. if High(fSSPIAuthContexts)>MAXSSPIAUTHCONTEXTS then begin
  47098. fServer.InternalLog(
  47099. 'Too many Windows Authenticated session in pending state: MAXSSPIAUTHCONTEXTS=%',
  47100. [MAXSSPIAUTHCONTEXTS],sllUserAuth);
  47101. exit;
  47102. end;
  47103. SecCtxIdx := CtxArr.New; // add a new entry to fSSPIAuthContexts[]
  47104. InvalidateSecContext(fSSPIAuthContexts[SecCtxIdx],ConnectionID);
  47105. end;
  47106. // call SSPI provider
  47107. if ServerSSPIAuth(fSSPIAuthContexts[SecCtxIdx], Base64ToBin(InDataEnc), OutData) then begin
  47108. if BrowserAuth then begin
  47109. Ctxt.Call.OutHead := (SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData);
  47110. Ctxt.Call.OutStatus := HTML_UNAUTHORIZED;
  47111. StatusCodeToErrorMsg(Ctxt.Call.OutStatus, Ctxt.Call.OutBody);
  47112. end else
  47113. Ctxt.Returns(['result','','data',BinToBase64(OutData)]);
  47114. exit; // 1st call: send back OutData to the client
  47115. end;
  47116. // 2nd call: user was authenticated -> release used context
  47117. ServerSSPIAuthUser(fSSPIAuthContexts[SecCtxIdx],UserName);
  47118. {$ifdef WITHLOG}
  47119. if sllUserAuth in fServer.fLogFamily.Level then
  47120. fServer.fLogFamily.SynLog.Log(sllUserAuth,'% Authentication success for %',
  47121. [SecPackageName(fSSPIAuthContexts[SecCtxIdx]),UserName],self);
  47122. {$endif}
  47123. // now client is authenticated -> create a session for aUserName
  47124. // and send back OutData
  47125. try
  47126. if UserName='' then
  47127. exit;
  47128. User := GetUser(Ctxt,UserName);
  47129. if User<>nil then
  47130. try
  47131. User.PasswordHashHexa := ''; // override with context
  47132. fServer.SessionCreate(User,Ctxt,Session); // call Ctxt.AuthenticationFailed on error
  47133. if Session<>nil then
  47134. with Session.User do
  47135. if BrowserAuth then
  47136. Ctxt.Returns(JSONEncode(['result',Session.fPrivateSalt,
  47137. 'logonid',IDValue,'logonname',LogonName,'logondisplay',DisplayName,
  47138. 'logongroup',GroupRights.IDValue,
  47139. 'server',ExeVersion.ProgramName,'version',ExeVersion.Version.Detailed]),
  47140. HTML_SUCCESS,(SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData)) else
  47141. Ctxt.Returns([
  47142. 'result',BinToBase64(SecEncrypt(fSSPIAuthContexts[SecCtxIdx],Session.fPrivateSalt)),
  47143. 'logonid',IDValue,'logonname',LogonName,'logondisplay',DisplayName,
  47144. 'logongroup',GroupRights.ID,'server',ExeVersion.ProgramName,
  47145. 'version',ExeVersion.Version.Detailed,'data',BinToBase64(OutData)]);
  47146. finally
  47147. User.Free;
  47148. end else
  47149. Ctxt.AuthenticationFailed(afUnknownUser);
  47150. finally
  47151. FreeSecContext(fSSPIAuthContexts[SecCtxIdx]);
  47152. CtxArr.Delete(SecCtxIdx);
  47153. end;
  47154. end;
  47155. class function TSQLRestServerAuthenticationSSPI.ClientComputeSessionKey(
  47156. Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8;
  47157. var SecCtx: TSecContext;
  47158. OutData: RawByteString;
  47159. begin
  47160. result := '';
  47161. InvalidateSecContext(SecCtx,0);
  47162. Sender.fSessionData := '';
  47163. try
  47164. repeat
  47165. if User.LogonName<>'' then
  47166. ClientSSPIAuthWithPassword(SecCtx,Sender.fSessionData,
  47167. User.LogonName,User.PasswordHashHexa,OutData) else
  47168. ClientSSPIAuth(SecCtx,Sender.fSessionData,User.PasswordHashHexa,OutData);
  47169. if OutData='' then
  47170. break;
  47171. if result<>'' then
  47172. break; // 2nd pass
  47173. // 1st call will return data, 2nd call SessionKey
  47174. result := ClientGetSessionKey(Sender,User,['UserName','','data',BinToBase64(OutData)]);
  47175. until Sender.fSessionData='';
  47176. if result<>'' then
  47177. result := SecDecrypt(SecCtx,Base64ToBin(result));
  47178. finally
  47179. FreeSecContext(SecCtx);
  47180. end;
  47181. // authenticated by Windows on the server side: use the returned
  47182. // SessionKey + PasswordHashHexa to sign the URI, as usual
  47183. User.PasswordHashHexa := ''; // should not appear on URI signature
  47184. end;
  47185. constructor TSQLRestServerAuthenticationSSPI.Create(aServer: TSQLRestServer);
  47186. begin
  47187. inherited Create(aServer);
  47188. end;
  47189. destructor TSQLRestServerAuthenticationSSPI.Destroy;
  47190. var i: integer;
  47191. begin
  47192. for i := 0 to High(fSSPIAuthContexts) do
  47193. FreeSecContext(fSSPIAuthContexts[i]);
  47194. inherited;
  47195. end;
  47196. {$endif SSPIAUTH}
  47197. { TSynAuthenticationRest }
  47198. constructor TSynAuthenticationRest.Create(aServer: TSQLRestServer;
  47199. const aAllowedGroups: array of integer);
  47200. begin
  47201. inherited Create;
  47202. fServer := aServer;
  47203. RegisterAllowedGroups(aAllowedGroups);
  47204. end;
  47205. procedure TSynAuthenticationRest.RegisterAllowedGroups(
  47206. const aAllowedGroups: array of integer);
  47207. var i: integer;
  47208. begin
  47209. for i := 0 to high(aAllowedGroups) do
  47210. AddSortedInteger(fAllowedGroups,aAllowedGroups[i]);
  47211. end;
  47212. function TSynAuthenticationRest.GetPassword(const UserName: RawUTF8;
  47213. out Password: RawUTF8): boolean;
  47214. var U: TSQLAuthUser;
  47215. begin
  47216. if fServer=nil then begin
  47217. result := false;
  47218. exit;
  47219. end;
  47220. U := fServer.fSQLAuthUserClass.Create(fServer,'LogonName=?',[UserName]);
  47221. try
  47222. result := (U.fID>0) and
  47223. (FastFindIntegerSorted(fAllowedGroups,PtrInt(U.fGroup))>=0);
  47224. if result then
  47225. Password := U.PasswordHashHexa; // same as ComputeHash() below
  47226. finally
  47227. U.Free;
  47228. end;
  47229. end;
  47230. function TSynAuthenticationRest.GetUsersCount: integer;
  47231. begin
  47232. result := 1; // fake answer indicating that authentication is enabled
  47233. end;
  47234. class function TSynAuthenticationRest.ComputeHash(Token: Int64;
  47235. const UserName,PassWord: RawUTF8): cardinal;
  47236. begin // same as GetPassword() above
  47237. result := inherited ComputeHash(Token,UserName,
  47238. TSQLAuthUser.ComputeHashedPassword(Password));
  47239. end;
  47240. { TServiceContainer }
  47241. function TServiceContainer.AddInterface(
  47242. const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation;
  47243. aContractExpected: RawUTF8): boolean;
  47244. var i: integer;
  47245. F: TServiceFactoryClient;
  47246. begin
  47247. result := false;
  47248. if (self=nil) or (high(aInterfaces)<0) then
  47249. exit;
  47250. CheckInterface(aInterfaces);
  47251. for i := 0 to high(aInterfaces) do begin
  47252. F := TServiceFactoryClient.Create(
  47253. Rest,aInterfaces[i],aInstanceCreation,aContractExpected);
  47254. AddServiceInternal(F);
  47255. aContractExpected := ''; // supplied contract is only for the 1st interface
  47256. end;
  47257. result := true;
  47258. end;
  47259. function TServiceContainer.AddInterface(aInterface: PTypeInfo;
  47260. aInstanceCreation: TServiceInstanceImplementation;
  47261. const aContractExpected: RawUTF8=''): TServiceFactoryClient;
  47262. begin
  47263. CheckInterface([aInterface]);
  47264. result := TServiceFactoryClient.Create(Rest,aInterface,aInstanceCreation,aContractExpected);
  47265. AddServiceInternal(result);
  47266. end;
  47267. function TServiceContainer.Count: integer;
  47268. begin
  47269. if self=nil then
  47270. result := 0 else
  47271. result := fList.Count;
  47272. end;
  47273. constructor TServiceContainer.Create(aRest: TSQLRest);
  47274. begin
  47275. fRest := aRest;
  47276. fList := TRawUTF8ListHashed.Create;
  47277. fList.CaseSensitive := false;
  47278. fListInterfaceMethods.InitSpecific(TypeInfo(TServiceContainerInterfaceMethods),
  47279. fListInterfaceMethod,djRawUTF8,nil,true);
  47280. end;
  47281. destructor TServiceContainer.Destroy;
  47282. var i: integer;
  47283. begin
  47284. for i := 0 to fList.Count-1 do
  47285. fList.Objects[i].Free;
  47286. fList.Free;
  47287. inherited;
  47288. end;
  47289. procedure TServiceContainer.Release;
  47290. begin
  47291. if (self<>nil) and (fRest<>nil) and (fRest.fServices=self) then
  47292. FreeAndNil(fRest.fServices);
  47293. end;
  47294. function TServiceContainer.AddServiceInternal(aService: TServiceFactory): integer;
  47295. var MethodIndex: integer;
  47296. procedure AddOne(const aInterfaceDotMethodName: RawUTF8);
  47297. begin
  47298. with PServiceContainerInterfaceMethod(fListInterfaceMethods.AddUniqueName(
  47299. aInterfaceDotMethodName,'',[]))^ do begin
  47300. InterfaceService := aService;
  47301. InterfaceMethodIndex := MethodIndex;
  47302. end;
  47303. inc(MethodIndex);
  47304. end;
  47305. var aURI: RawUTF8;
  47306. internal: TServiceInternalMethod;
  47307. m: integer;
  47308. begin
  47309. if (self=nil) or (aService=nil) then
  47310. result := 0 else
  47311. with aService do begin
  47312. // add service factory
  47313. if ExpectMangledURI then
  47314. aURI := fInterfaceMangledURI else
  47315. aURI := fInterfaceURI;
  47316. result := fList.AddObject(aURI,aService);
  47317. // add associated methods
  47318. aURI := aURI+'.';
  47319. MethodIndex := 0;
  47320. for internal := Low(TServiceInternalMethod) to High(TServiceInternalMethod) do
  47321. AddOne(aURI+SERVICE_PSEUDO_METHOD[internal]);
  47322. for m := 0 to fInterface.fMethodsCount-1 do
  47323. AddOne(aURI+fInterface.fMethods[m].URI);
  47324. end;
  47325. end;
  47326. procedure TServiceContainer.CheckInterface(const aInterfaces: array of PTypeInfo);
  47327. var i: integer;
  47328. begin
  47329. for i := 0 to high(aInterfaces) do
  47330. if aInterfaces[i]=nil then
  47331. raise EServiceException.CreateUTF8('%: aInterfaces[%]=nil',[self,i]) else
  47332. with aInterfaces[i]^, PInterfaceTypeData(ClassType)^ do
  47333. if Kind<>tkInterface then
  47334. raise EServiceException.CreateUTF8('%: % is not an interface',[self,Name]) else
  47335. if not (ifHasGuid in IntfFlags) then
  47336. raise EServiceException.CreateUTF8('%: % interface has no GUID',[self,Name]) else
  47337. if Info(IntfGuid)<>nil then
  47338. raise EServiceException.CreateUTF8('%: % GUID already registered',[self,Name]);
  47339. end;
  47340. procedure TServiceContainer.SetExpectMangledURI(aValue: Boolean);
  47341. var f: Integer;
  47342. Fac: array of TServiceFactory;
  47343. begin
  47344. if aValue=fExpectMangledURI then
  47345. exit;
  47346. fExpectMangledURI := aValue;
  47347. fList.CaseSensitive := aValue;
  47348. SetLength(Fac,fList.Count);
  47349. for f := 0 to fList.Count-1 do
  47350. Fac[f] := fList.Objects[f] as TServiceFactory;
  47351. fList.Clear;
  47352. fListInterfaceMethod := nil;
  47353. fListInterfaceMethods.InitSpecific(TypeInfo(TServiceContainerInterfaceMethods),
  47354. fListInterfaceMethod,djRawUTF8,nil,not aValue);
  47355. for f := 0 to High(Fac) do
  47356. AddServiceInternal(Fac[f]);
  47357. end;
  47358. procedure TServiceContainer.SetInterfaceMethodBits(MethodNamesCSV: PUTF8Char;
  47359. IncludePseudoMethods: boolean; out bits: TServiceContainerInterfaceMethodBits);
  47360. var i,n: integer;
  47361. method: RawUTF8;
  47362. begin
  47363. FillcharFast(bits,sizeof(bits),0);
  47364. n := length(fListInterfaceMethod);
  47365. if n>sizeof(bits) shl 3 then
  47366. raise EServiceException.CreateUTF8('%.SetInterfaceMethodBits: n=%',[self,n]);
  47367. if IncludePseudoMethods then
  47368. for i := 0 to n-1 do
  47369. if fListInterfaceMethod[i].InterfaceMethodIndex<SERVICE_PSEUDO_METHOD_COUNT then
  47370. include(bits,i);
  47371. while MethodNamesCSV<>nil do begin
  47372. method := GetNextItem(MethodNamesCSV);
  47373. if PosEx('.',method)=0 then begin
  47374. for i := 0 to n-1 do
  47375. with fListInterfaceMethod[i] do // O(n) search is fast enough here
  47376. if (InterfaceMethodIndex>=SERVICE_PSEUDO_METHOD_COUNT) and
  47377. IdemPropNameU(method,InterfaceService.fInterface.
  47378. fMethods[InterfaceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT].URI) then
  47379. include(bits,i);
  47380. end else begin
  47381. i := fListInterfaceMethods.FindHashed(method); // O(1) search
  47382. if i>=0 then
  47383. include(bits,i);
  47384. end;
  47385. end;
  47386. end;
  47387. function TServiceContainer.GetMethodName(ListInterfaceMethodIndex: integer): RawUTF8;
  47388. begin
  47389. if cardinal(ListInterfaceMethodIndex)>=cardinal(length(fListInterfaceMethod)) then
  47390. result := '' else
  47391. with fListInterfaceMethod[ListInterfaceMethodIndex] do
  47392. result := InterfaceService.fInterface.GetMethodName(InterfaceMethodIndex);
  47393. end;
  47394. function TServiceContainer.GetService(const aURI: RawUTF8): TServiceFactory;
  47395. var i: Integer;
  47396. begin
  47397. if (self<>nil) and (aURI<>'') then begin
  47398. i := fList.IndexOf(aURI);
  47399. if i>=0 then
  47400. result := TServiceFactory(fList.Objects[i]) else
  47401. result := nil;
  47402. end else
  47403. result := nil;
  47404. end;
  47405. function TServiceContainer.Info(aTypeInfo: PTypeInfo): TServiceFactory;
  47406. var i: Integer;
  47407. Obj: PPointerArray;
  47408. begin
  47409. if self<>nil then begin
  47410. Obj := fList.ObjectPtr;
  47411. for i := 0 to fList.Count-1 do begin
  47412. result := Obj[i];
  47413. if result.fInterface.fInterfaceTypeInfo=aTypeInfo then
  47414. exit;
  47415. end;
  47416. end;
  47417. result := nil;
  47418. end;
  47419. function TServiceContainer.Info(const aGUID: TGUID): TServiceFactory;
  47420. var i: Integer;
  47421. Obj: PPointerArray;
  47422. begin
  47423. if self<>nil then begin
  47424. Obj := fList.ObjectPtr;
  47425. for i := 0 to fList.Count-1 do begin
  47426. result := Obj[i];
  47427. if IsEqualGUID(result.fInterface.fInterfaceIID,aGUID) then
  47428. exit;
  47429. end;
  47430. end;
  47431. result := nil;
  47432. end;
  47433. procedure TServiceContainer.SetGUIDs(out Services: TGUIDDynArray);
  47434. var i: Integer;
  47435. begin
  47436. if self=nil then
  47437. exit;
  47438. SetLength(Services,fList.Count);
  47439. for i := 0 to fList.Count-1 do
  47440. Services[i] := TServiceFactory(fList.ObjectPtr[i]).fInterface.fInterfaceIID;
  47441. end;
  47442. procedure TServiceContainer.SetInterfaceNames(out Names: TRawUTF8DynArray);
  47443. var i: Integer;
  47444. begin
  47445. if self=nil then
  47446. exit;
  47447. SetLength(Names,fList.Count);
  47448. for i := 0 to fList.Count-1 do
  47449. Names[i] := TServiceFactory(fList.ObjectPtr[i]).fInterfaceURI;
  47450. end;
  47451. function TServiceContainer.AsJson: RawJSON;
  47452. var WR: TTextWriter;
  47453. i: integer;
  47454. begin
  47455. result := '';
  47456. if (self=nil) or (fList.Count=0) then
  47457. exit;
  47458. WR := TJSONSerializer.CreateOwnedStream;
  47459. try
  47460. WR.Add('[');
  47461. for i := 0 to fList.Count-1 do begin
  47462. WR.AddString(TServiceFactory(fList.ObjectPtr[i]).Contract);
  47463. WR.Add(',');
  47464. end;
  47465. WR.CancelLastComma;
  47466. WR.Add(']');
  47467. WR.SetText(RawUTF8(result));
  47468. finally
  47469. WR.Free;
  47470. end;
  47471. end;
  47472. function TServiceContainer.TryResolve(aInterface: PTypeInfo; out Obj): boolean;
  47473. var factory: TServiceFactory;
  47474. begin
  47475. factory := Info(aInterface);
  47476. if factory=nil then
  47477. result := inherited TryResolve(aInterface,Obj) else
  47478. result := factory.Get(Obj);
  47479. end;
  47480. function TServiceContainer.Index(aIndex: integer): TServiceFactory;
  47481. begin
  47482. if Self=nil then
  47483. result := nil else
  47484. result := TServiceFactory(fList.Objects[aIndex]);
  47485. end;
  47486. function TServiceContainer.CallBackUnRegister(const Callback: IInvokable): boolean;
  47487. begin
  47488. result := false; // nothing to be done here
  47489. end;
  47490. { TInterfacedObjectFake }
  47491. const
  47492. // this is used to avoid creating dynamic arrays if not needed
  47493. MAX_METHOD_ARGS = 32;
  47494. // QueryInterface, _AddRef and _Release methods are hard-coded
  47495. RESERVED_VTABLE_SLOTS = 3;
  47496. // see http://docwiki.embarcadero.com/RADStudio/en/Program_Control
  47497. {$ifdef CPU64}
  47498. // maximum stack size at method execution must match .PARAMS 64 (minus 4 regs)
  47499. MAX_EXECSTACK = 60*8;
  47500. {$else}
  47501. // maximum stack size at method execution
  47502. {$ifdef CPUARM}
  47503. MAX_EXECSTACK = 60*4;
  47504. {$else}
  47505. MAX_EXECSTACK = 1024;
  47506. {$endif}
  47507. {$endif CPU64}
  47508. {$ifdef CPUX86}
  47509. // 32-bit integer param registers (in "register" calling convention)
  47510. REGEAX = 1;
  47511. REGEDX = 2;
  47512. REGECX = 3;
  47513. PARAMREG_FIRST = REGEAX;
  47514. PARAMREG_LAST = REGECX;
  47515. // floating-point params are passed by reference
  47516. {$endif CPUX86}
  47517. {$ifdef CPUX64}
  47518. // 64-bit integer param registers
  47519. {$ifdef LINUX}
  47520. REGRDI = 1;
  47521. REGRSI = 2;
  47522. REGRDX = 3;
  47523. REGRCX = 4;
  47524. REGR8 = 5;
  47525. REGR9 = 6;
  47526. PARAMREG_FIRST = REGRDI;
  47527. PARAMREG_RESULT = REGRSI;
  47528. {$else}
  47529. REGRCX = 1;
  47530. REGRDX = 2;
  47531. REGR8 = 3;
  47532. REGR9 = 4;
  47533. PARAMREG_FIRST = REGRCX;
  47534. PARAMREG_RESULT = REGRDX;
  47535. {$endif}
  47536. PARAMREG_LAST = REGR9;
  47537. // 64-bit floating-point (double) registers
  47538. REGXMM0 = 1;
  47539. REGXMM1 = 2;
  47540. REGXMM2 = 3;
  47541. REGXMM3 = 4;
  47542. {$ifdef LINUX}
  47543. REGXMM4 = 5;
  47544. REGXMM5 = 6;
  47545. REGXMM6 = 7;
  47546. REGXMM7 = 8;
  47547. FPREG_FIRST = REGXMM0;
  47548. FPREG_LAST = REGXMM7;
  47549. {$else}
  47550. FPREG_FIRST = REGXMM0;
  47551. FPREG_LAST = REGXMM3;
  47552. {$endif}
  47553. {$define HAS_FPREG}
  47554. {$endif CPUX64}
  47555. {$ifdef CPUARM}
  47556. // 32-bit integer param registers
  47557. REGR0 = 1;
  47558. REGR1 = 2;
  47559. REGR2 = 3;
  47560. REGR3 = 4;
  47561. PARAMREG_FIRST = REGR0;
  47562. PARAMREG_LAST = REGR3;
  47563. PARAMREG_RESULT = REGR1;
  47564. // 64-bit floating-point (double) registers
  47565. REGD0 = 1;
  47566. REGD1 = 2;
  47567. REGD2 = 3;
  47568. REGD3 = 4;
  47569. REGD4 = 5;
  47570. REGD5 = 6;
  47571. REGD6 = 7;
  47572. REGD7 = 8;
  47573. FPREG_FIRST = REGD0;
  47574. FPREG_LAST = REGD7;
  47575. {$define HAS_FPREG}
  47576. {$endif CPUARM}
  47577. {$ifdef CPUAARCH64}
  47578. // 64-bit integer param registers
  47579. REGX0 = 1;
  47580. REGX1 = 2;
  47581. REGX2 = 3;
  47582. REGX3 = 4;
  47583. REGX4 = 5;
  47584. REGX5 = 6;
  47585. REGX6 = 7;
  47586. REGX7 = 8;
  47587. PARAMREG_FIRST = REGX0;
  47588. PARAMREG_LAST = REGX7;
  47589. PARAMREG_RESULT = REGX0; // is really REGX1 self?
  47590. // 64-bit floating-point (double) registers
  47591. REGD0 = 1; // map REGV0 128-bit NEON register
  47592. REGD1 = 2; // REGV1
  47593. REGD2 = 3; // REGV2
  47594. REGD3 = 4; // REGV3
  47595. REGD4 = 5; // REGV4
  47596. REGD5 = 6; // REGV5
  47597. REGD6 = 7; // REGV6
  47598. REGD7 = 8; // REGV7
  47599. FPREG_FIRST = REGD0;
  47600. FPREG_LAST = REGD7;
  47601. {$define HAS_FPREG}
  47602. {$endif CPUAARCH64}
  47603. PTRSIZ = sizeof(Pointer);
  47604. PTRSHR = {$ifdef CPU64}3{$else}2{$endif};
  47605. STACKOFFSET_NONE = -1;
  47606. // ordinal values are stored within 64-bit buffer, and records in a RawUTF8
  47607. CONST_ARGS_TO_VAR: array[TServiceMethodValueType] of TServiceMethodValueVar = (
  47608. smvvNone, smvvSelf, smvv64, smvv64, smvv64, smvv64, smvv64, smvv64, smvv64,
  47609. smvv64, smvv64,
  47610. smvvRawUTF8, smvvString, smvvRawUTF8, smvvWideString, smvvRecord,
  47611. {$ifndef NOVARIANTS}smvvRecord,{$endif} smvvObject, smvvRawUTF8,
  47612. smvvDynArray, smvvInterface);
  47613. {$ifdef CPU32}
  47614. // always aligned to 8 bytes boundaries for 64-bit
  47615. CONST_ARGS_IN_STACK_SIZE: array[TServiceMethodValueType] of Cardinal = (
  47616. 0, PTRSIZ,PTRSIZ, PTRSIZ,PTRSIZ,PTRSIZ, PTRSIZ, 8, 8, 8,
  47617. // None, Self, Boolean, Enum, Set, Integer, Cardinal, Int64, Double, DateTime,
  47618. 8, PTRSIZ, PTRSIZ, PTRSIZ, PTRSIZ, PTRSIZ,
  47619. // Currency, RawUTF8, String, RawByteString, WideString, Record,
  47620. {$ifndef NOVARIANTS}PTRSIZ,{$endif} // Variant
  47621. PTRSIZ, PTRSIZ, PTRSIZ, PTRSIZ);
  47622. // Object, RawJSON, DynArray, Interface
  47623. {$endif}
  47624. CONST_ARGS_RESULT_BY_REF: TServiceMethodValueTypes = [
  47625. smvRawUTF8, smvRawJSON, smvString, smvRawByteString, smvWideString, smvRecord,
  47626. {$ifndef NOVARIANTS}smvVariant,{$endif} smvDynArray];
  47627. CONST_PSEUDO_RESULT_NAME: string[6] = 'Result';
  47628. CONST_PSEUDO_SELF_NAME: string[4] = 'Self';
  47629. CONST_INTEGER_NAME: string[7] = 'Integer';
  47630. type
  47631. /// map the stack memory layout at TInterfacedObjectFake.FakeCall()
  47632. TFakeCallStack = packed record
  47633. {$ifdef CPUX86}
  47634. EDX, ECX, MethodIndex, EBP, Ret: cardinal;
  47635. {$else}
  47636. {$ifdef Linux}
  47637. ParamRegs: packed array[PARAMREG_FIRST..PARAMREG_LAST] of pointer;
  47638. {$endif}
  47639. {$ifdef HAS_FPREG}
  47640. FPRegs: packed array[FPREG_FIRST..FPREG_LAST] of double;
  47641. {$endif}
  47642. MethodIndex: PtrUInt;
  47643. Frame: pointer;
  47644. Ret: pointer;
  47645. {$ifndef Linux}
  47646. ParamRegs: packed array[PARAMREG_FIRST..PARAMREG_LAST] of pointer;
  47647. {$endif}
  47648. {$endif CPUX86}
  47649. {$ifdef CPUARM}
  47650. // alf: on ARM, there is more on the stack than you would expect
  47651. DummyStack: packed array[0..9] of pointer;
  47652. {$endif}
  47653. {$ifdef CPUAARCH64}
  47654. // alf: on AARCH64, there is more on the stack than you would expect
  47655. DummyStack: packed array[0..0] of pointer;
  47656. {$endif}
  47657. Stack: packed array[word] of byte;
  47658. end;
  47659. /// instances of this class will emulate a given interface
  47660. // - as used by TInterfaceFactory.CreateFakeInstance
  47661. TInterfacedObjectFake = class(TInterfacedObjectFromFactory)
  47662. protected
  47663. fVTable: PPointerArray;
  47664. function FakeCall(var aCall: TFakeCallStack): Int64;
  47665. {$ifdef FPC}
  47666. {$ifdef CPUARM}
  47667. // on ARM, the FakeStub needs to be here, otherwise the FakeCall cannot be found by the FakeStub
  47668. procedure ArmFakeStub;
  47669. {$endif}
  47670. {$ifdef CPUAARCH64}
  47671. // on Aarch64, the FakeStub needs to be here, otherwise the FakeCall cannot be found by the FakeStub
  47672. procedure AArch64FakeStub;
  47673. {$endif}
  47674. function FakeQueryInterface(
  47675. {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID;
  47676. out Obj): longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  47677. function Fake_AddRef: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  47678. function Fake_Release: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  47679. {$else}
  47680. function FakeQueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
  47681. function Fake_AddRef: Integer; stdcall;
  47682. function Fake_Release: Integer; stdcall;
  47683. {$endif}
  47684. function SelfFromInterface: TInterfacedObjectFake;
  47685. {$ifdef PUREPASCAL} {$ifdef HASINLINE}inline;{$endif} {$endif}
  47686. procedure InterfaceWrite(W: TJSONSerializer; const aMethod: TServiceMethod;
  47687. const aParamInfo: TServiceMethodArgument; aParamValue: Pointer); virtual;
  47688. public
  47689. /// create an instance, using the specified interface
  47690. constructor Create(aFactory: TInterfaceFactory;
  47691. aOptions: TInterfacedObjectFromFactoryOptions;
  47692. aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy);
  47693. /// retrieve one local instance of this interface
  47694. procedure Get(out Obj);
  47695. end;
  47696. TInterfacedObjectFakeClient = class(TInterfacedObjectFake)
  47697. protected
  47698. fClient: TServiceFactoryClient;
  47699. procedure InterfaceWrite(W: TJSONSerializer; const aMethod: TServiceMethod;
  47700. const aParamInfo: TServiceMethodArgument; aParamValue: Pointer); override;
  47701. public
  47702. constructor Create(aClient: TServiceFactoryClient;
  47703. aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy);
  47704. destructor Destroy; override;
  47705. end;
  47706. TInterfacedObjectFakeServer = class(TInterfacedObjectFake)
  47707. protected
  47708. fServer: TSQLRestServer;
  47709. fLowLevelConnectionID: Int64;
  47710. fService: TServiceFactoryServer;
  47711. fReleasedOnClientSide: boolean;
  47712. fFakeInterface: Pointer;
  47713. fRaiseExceptionOnInvokeError: boolean;
  47714. function CallbackInvoke(const aMethod: TServiceMethod;
  47715. const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8;
  47716. aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; virtual;
  47717. public
  47718. constructor Create(aRequest: TSQLRestServerURIContext;
  47719. aFactory: TInterfaceFactory; aFakeID: Integer);
  47720. destructor Destroy; override;
  47721. end;
  47722. EInterfaceStub = class(EInterfaceFactoryException)
  47723. public
  47724. constructor Create(Sender: TInterfaceStub; const Method: TServiceMethod;
  47725. const Error: RawUTF8); overload;
  47726. constructor Create(Sender: TInterfaceStub; const Method: TServiceMethod;
  47727. const Format: RawUTF8; const Args: array of const); overload;
  47728. end;
  47729. constructor TInterfacedObjectFake.Create(aFactory: TInterfaceFactory;
  47730. aOptions: TInterfacedObjectFromFactoryOptions; aInvoke: TOnFakeInstanceInvoke;
  47731. aNotifyDestroy: TOnFakeInstanceDestroy);
  47732. begin
  47733. inherited Create(aFactory,aOptions,aInvoke,aNotifyDestroy);
  47734. fVTable := aFactory.GetMethodsVirtualTable;
  47735. end;
  47736. function TInterfacedObjectFake.SelfFromInterface: TInterfacedObjectFake;
  47737. {$ifdef PUREPASCAL}
  47738. begin
  47739. result := pointer(PtrInt(self)-PtrInt(@TInterfacedObjectFake(nil).fVTable));
  47740. end;
  47741. {$else}
  47742. {$ifdef CPUINTEL}
  47743. asm
  47744. sub eax,TInterfacedObjectFake.fVTable
  47745. end;
  47746. {$endif CPUINTEL}
  47747. {$endif}
  47748. function TInterfacedObjectFake.Fake_AddRef: {$ifdef FPC}longint{$else}integer{$endif};
  47749. begin
  47750. result := SelfFromInterface._AddRef;
  47751. end;
  47752. function TInterfacedObjectFake.Fake_Release: {$ifdef FPC}longint{$else}integer{$endif};
  47753. begin
  47754. result := SelfFromInterface._Release;
  47755. end;
  47756. function TInterfacedObjectFake.FakeQueryInterface(
  47757. {$ifdef FPC}
  47758. {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID; out Obj): longint;
  47759. {$else}
  47760. const IID: TGUID; out Obj): HResult;
  47761. {$endif}
  47762. begin
  47763. self := SelfFromInterface;
  47764. if IsEqualGUID(IID,fFactory.fInterfaceIID) then begin
  47765. pointer(Obj) := @fVTable;
  47766. _AddRef;
  47767. result := NOERROR;
  47768. end else
  47769. if GetInterface(IID,Obj) then
  47770. result := NOERROR else
  47771. result := {$ifdef FPC}longint{$endif}(E_NOINTERFACE);
  47772. end;
  47773. procedure TInterfacedObjectFake.Get(out Obj);
  47774. begin
  47775. pointer(Obj) := @fVTable;
  47776. _AddRef;
  47777. end;
  47778. procedure IgnoreComma(var P: PUTF8Char);
  47779. begin
  47780. if P<>nil then begin
  47781. if P^ in [#1..' '] then repeat inc(P) until not(P^ in [#1..' ']);
  47782. if P^=',' then inc(P);
  47783. end;
  47784. end;
  47785. function TInterfacedObjectFake.FakeCall(var aCall: TFakeCallStack): Int64;
  47786. var method: ^TServiceMethod;
  47787. procedure RaiseError(const Format: RawUTF8; const Args: array of const);
  47788. var msg: RawUTF8;
  47789. begin
  47790. msg := FormatUTF8(Format,Args);
  47791. raise EInterfaceFactoryException.CreateUTF8('%.FakeCall(%.%) failed: %',
  47792. [self,fFactory.fInterfaceTypeInfo^.Name,method^.URI,msg]);
  47793. end;
  47794. var resultType: TServiceMethodValueType; // type of value stored into result
  47795. procedure InternalProcess;
  47796. var Params: TJSONSerializer;
  47797. Error, ResArray, ParamsJSON: RawUTF8;
  47798. arg, ValLen: integer;
  47799. V: PPointer;
  47800. R, Val: PUTF8Char;
  47801. valid, wasString, resultAsJSONObject: boolean;
  47802. ServiceCustomAnswerPoint: PServiceCustomAnswer;
  47803. DynArrays: array[0..MAX_METHOD_ARGS-1] of TDynArray;
  47804. Value: array[0..MAX_METHOD_ARGS-1] of pointer;
  47805. I64s: array[0..MAX_METHOD_ARGS-1] of Int64;
  47806. begin
  47807. Params := TJSONSerializer.CreateOwnedStream;
  47808. try
  47809. // create the parameters
  47810. if ifoJsonAsExtended in fOptions then
  47811. include(Params.fCustomOptions,twoForceJSONExtended) else
  47812. include(Params.fCustomOptions,twoForceJSONStandard); // e.g. for AJAX
  47813. FillcharFast(I64s,method^.ArgsUsedCount[smvv64]*sizeof(Int64),0);
  47814. for arg := 1 to high(method^.Args) do
  47815. with method^.Args[arg] do
  47816. if ValueType>smvSelf then begin
  47817. {$ifdef HAS_FPREG} // x64, arm, aarch64
  47818. if FPRegisterIdent>0 then
  47819. V := Pointer((PtrUInt(@aCall.FPRegs[FPREG_FIRST])+Sizeof(Double)*(FPRegisterIdent-1))) else
  47820. if RegisterIdent>0 then
  47821. V := Pointer((PtrUInt(@aCall.ParamRegs[PARAMREG_FIRST])+Sizeof(pointer)*(RegisterIdent-1))) else
  47822. {$endif}
  47823. V := nil;
  47824. {$ifndef CPUAARCH64} // on aarch64, reference result can be in PARAMREG_FIRST
  47825. if RegisterIdent=PARAMREG_FIRST then
  47826. RaiseError('unexpected self',[]);
  47827. {$endif}
  47828. {$ifdef CPUX86}
  47829. case RegisterIdent of
  47830. REGEAX: RaiseError('unexpected self',[]);
  47831. REGEDX: V := @aCall.EDX;
  47832. REGECX: V := @aCall.ECX;
  47833. else
  47834. {$endif}
  47835. if V=nil then
  47836. if (SizeInStack>0) and (InStackOffset<>STACKOFFSET_NONE) then
  47837. V := @aCall.Stack[InStackOffset] else
  47838. V := @I64s[IndexVar]; // for results in CPU
  47839. {$ifdef CPUX86}
  47840. end;
  47841. {$endif}
  47842. if vPassedByReference in ValueKindAsm then
  47843. V := PPointer(V)^;
  47844. case ValueType of
  47845. smvDynArray:
  47846. {$ifdef FPC} // FIXME ?
  47847. if vIsObjArray in ValueKindAsm then
  47848. DynArrays[IndexVar].Init(ArgTypeInfo,V^) else
  47849. DynArrays[IndexVar].Init(ArgTypeInfo,V);
  47850. {$else}
  47851. DynArrays[IndexVar].Init(ArgTypeInfo,V^);
  47852. {$endif}
  47853. end;
  47854. Value[arg] := V;
  47855. if ValueDirection in [smdConst,smdVar] then
  47856. case ValueType of
  47857. smvInterface:
  47858. InterfaceWrite(Params,method^,method^.Args[arg],V^);
  47859. smvDynArray: begin
  47860. Params.AddDynArrayJSON(DynArrays[IndexVar]);
  47861. Params.Add(',');
  47862. end;
  47863. else AddJSON(Params,V);
  47864. end;
  47865. end;
  47866. Params.CancelLastComma;
  47867. Params.SetText(ParamsJSON);
  47868. // call remote server or stub implementation
  47869. if method^.ArgsResultIsServiceCustomAnswer then
  47870. ServiceCustomAnswerPoint := Value[method^.ArgsResultIndex] else
  47871. ServiceCustomAnswerPoint := nil;
  47872. if not fInvoke(method^,ParamsJSON,
  47873. @ResArray,@Error,@fClientDrivenID,ServiceCustomAnswerPoint) then
  47874. RaiseError('''%''',[Error]);
  47875. finally
  47876. Params.Free;
  47877. end;
  47878. // retrieve method result and var/out parameters content
  47879. if ServiceCustomAnswerPoint=nil then
  47880. if ResArray<>'' then begin
  47881. R := pointer(ResArray);
  47882. if R^ in [#1..' '] then repeat inc(R) until not(R^ in [#1..' ']);
  47883. resultAsJSONObject := false; // [value,...] JSON array format
  47884. if R^='{' then // {"paramname":value,...} JSON object format
  47885. resultAsJSONObject := true else
  47886. if R^<>'[' then
  47887. RaiseError('JSON array/object result expected',[]);
  47888. inc(R);
  47889. arg := method^.ArgsOutFirst;
  47890. if arg>0 then
  47891. repeat
  47892. if resultAsJSONObject then begin
  47893. Val := GetJSONPropName(R);
  47894. if Val=nil then
  47895. break; // end of JSON object
  47896. ValLen := StrLen(Val);
  47897. if (arg>0) and not IdemPropName(method^.Args[arg].ParamName^,Val,ValLen) then begin
  47898. arg := method^.ArgIndex(Val,ValLen,false); // only if were not in-order
  47899. if arg<0 then
  47900. RaiseError('unexpected parameter "%"',[Val]);
  47901. end;
  47902. end;
  47903. with method^.Args[arg] do begin
  47904. //assert(ValueDirection in [smdVar,smdOut,smdResult]);
  47905. V := Value[arg];
  47906. case ValueType of
  47907. smvObject: begin
  47908. if PInteger(R)^=NULL_LOW then
  47909. inc(R,4) else begin // null from TInterfacedStub -> stay untouched
  47910. R := JSONToObject(V^,R,valid);
  47911. if not valid then
  47912. RaiseError('returned object',[]);
  47913. end;
  47914. IgnoreComma(R);
  47915. end;
  47916. smvInterface:
  47917. RaiseError('unexpected var/out interface',[]);
  47918. smvRawJSON:
  47919. if (R<>nil) and (R^=']') then
  47920. PRawUTF8(V)^ := '' else begin
  47921. GetJSONItemAsRawJSON(R,PRawJSON(V)^);
  47922. if R=nil then
  47923. RaiseError('returned RawJSON',[]);
  47924. end;
  47925. smvDynArray: begin
  47926. if vIsObjArray in ValueKindAsm then
  47927. ObjArrayClear(V^);
  47928. R := DynArrays[IndexVar].LoadFromJSON(R);
  47929. if R=nil then
  47930. RaiseError('returned array',[]);
  47931. IgnoreComma(R);
  47932. end;
  47933. smvBoolean..smvWideString: begin
  47934. Val := GetJSONField(R,R,@wasString);
  47935. if (Val=nil) or (wasString<>(vIsString in ValueKindAsm)) then
  47936. if resultAsJSONObject then
  47937. RaiseError('missing or invalid value',[]) else
  47938. RaiseError('missing or invalid value: '+
  47939. 'parameters shall follow method var/out/result order',[]);
  47940. if (ValueType=smvBoolean) and (PInteger(Val)^=TRUE_LOW) then
  47941. Val := '1'; // handle also BOOL with SizeInStorage=2
  47942. case ValueType of
  47943. smvBoolean, smvEnum, smvSet, smvCardinal:
  47944. case SizeInStorage of
  47945. 1: PByte(V)^ := GetCardinal(Val);
  47946. 2: PWord(V)^ := GetCardinal(Val);
  47947. 4: PCardinal(V)^ := GetCardinal(Val);
  47948. end;
  47949. smvInteger: PInteger(V)^ := GetInteger(Val);
  47950. smvInt64: SetInt64(Val,PInt64(V)^);
  47951. smvDouble,smvDateTime: PDouble(V)^ := GetExtended(Val);
  47952. smvCurrency: PInt64(V)^ := StrToCurr64(Val);
  47953. smvRawUTF8: SetString(PRawUTF8(V)^,PAnsiChar(Val),StrLen(Val));
  47954. smvString: UTF8DecodeToString(Val,StrLen(Val),PString(V)^);
  47955. smvRawByteString: Base64ToBin(PAnsiChar(Val),StrLen(Val),PRawByteString(V)^);
  47956. smvWideString: UTF8ToWideString(Val,StrLen(Val),PWideString(V)^);
  47957. else RaiseError('ValueType=%',[ord(ValueType)]);
  47958. end;
  47959. end;
  47960. smvRecord: begin
  47961. R := RecordLoadJSON(V^,R,ArgTypeInfo);
  47962. if R=nil then
  47963. RaiseError('returned record',[]);
  47964. end;
  47965. {$ifndef NOVARIANTS}
  47966. smvVariant: begin
  47967. R := VariantLoadJSON(PVariant(V)^,R,nil,@fFactory.DocVariantOptions);
  47968. if R=nil then
  47969. RaiseError('returned variant',[]);
  47970. end;
  47971. {$endif}
  47972. end;
  47973. if ValueDirection=smdResult then begin
  47974. resultType := ValueType;
  47975. if ValueType in [smvBoolean..smvCurrency] then
  47976. // ordinal/real result values to CPU/FPU registers
  47977. MoveFast(V^,Result,SizeInStorage);
  47978. end;
  47979. end;
  47980. if R=nil then
  47981. break;
  47982. if R^ in [#1..' '] then repeat inc(R) until not(R^ in [#1..' ']);
  47983. if resultAsJSONObject then begin
  47984. if (R^=#0) or (R^='}') then
  47985. break else // end of JSON object
  47986. if not method^.ArgNext(arg,false) then
  47987. arg := 0; // no next result argument -> force manual search
  47988. end else
  47989. if not method^.ArgNext(arg,false) then
  47990. break; // end of JSON array
  47991. until false;
  47992. end else
  47993. if method^.ArgsOutputValuesCount>0 then
  47994. RaiseError('method returned value, but ResArray=''''',[]);
  47995. end;
  47996. begin
  47997. // WELCOME ABOARD: you just landed in TInterfacedObjectFake.FakeCall() !
  47998. // if your debugger reached here, you are executing a "fake" interface
  47999. // forged to call a remote SOA server or mock/stub an interface
  48000. self := SelfFromInterface;
  48001. {$ifdef CPUAARCH64}
  48002. // alf: on aarch64, the self is sometimes only available in x1, when we have a result pointer !
  48003. // try to detect this ... although not very elegant, but I do not yet know how else to do this
  48004. try
  48005. if (fFactory=nil) or (fFactory.fDetectX0ResultMagic<>$AAAAAAAA) then begin
  48006. // aha, we have a reference result, placed in X0, so self is in X1 !!
  48007. self := aCall.ParamRegs[REGX1];
  48008. self := SelfFromInterface;
  48009. if fFactory.fDetectX0ResultMagic<>$AAAAAAAA then
  48010. raise EInterfaceFactoryException.CreateUTF8('Self error',[]);
  48011. end;
  48012. except
  48013. // if the above fails due to some error, we are definitely sure that the self is in REGX1 !!
  48014. self := aCall.ParamRegs[REGX1];
  48015. self := SelfFromInterface;
  48016. if fFactory.fDetectX0ResultMagic<>$AAAAAAAA then
  48017. raise EInterfaceFactoryException.CreateUTF8('Self error',[]);
  48018. end;
  48019. {$endif}
  48020. if aCall.MethodIndex>=fFactory.fMethodsCount then
  48021. raise EInterfaceFactoryException.CreateUTF8(
  48022. '%.FakeCall(%.%) failed: out of range method %>=%',
  48023. [self,fFactory.fInterfaceTypeInfo^.Name,aCall.MethodIndex,fFactory.fMethodsCount]);
  48024. method := @fFactory.fMethods[aCall.MethodIndex];
  48025. if not Assigned(fInvoke)then
  48026. RaiseError('fInvoke=nil',[]);
  48027. result := 0;
  48028. resultType := smvNone;
  48029. InternalProcess; // use an inner proc to ensure direct fld/fild FPU ops
  48030. case resultType of // al/ax/eax/eax:edx/rax already in result
  48031. {$ifdef HAS_FPREG}
  48032. smvDouble,smvDateTime: aCall.FPRegs[FPREG_FIRST] := PDouble(@result)^;
  48033. {$else}
  48034. smvDouble,smvDateTime: asm fld qword ptr [result] end; // in st(0)
  48035. smvCurrency: asm fild qword ptr [result] end; // in st(0)
  48036. {$endif}
  48037. end;
  48038. end;
  48039. procedure TInterfacedObjectFake.InterfaceWrite(W: TJSONSerializer;
  48040. const aMethod: TServiceMethod; const aParamInfo: TServiceMethodArgument;
  48041. aParamValue: Pointer);
  48042. begin
  48043. raise EInterfaceFactoryException.CreateUTF8('%: unhandled %.%(%: %) argument',
  48044. [self,fFactory.fInterfaceTypeInfo^.Name,aMethod.URI,
  48045. aParamInfo.ParamName^,aParamInfo.ArgTypeName^]);
  48046. end;
  48047. constructor TInterfacedObjectFakeClient.Create(aClient: TServiceFactoryClient;
  48048. aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy);
  48049. var opt: TInterfacedObjectFromFactoryOptions;
  48050. begin
  48051. fClient := aClient;
  48052. if (fClient.fClient<>nil) and (fClient.fClient.fSessionID<>0) then
  48053. opt := [ifoJsonAsExtended] else
  48054. opt := [];
  48055. inherited Create(aClient.fInterface,opt,aInvoke,aNotifyDestroy);
  48056. end;
  48057. procedure TInterfacedObjectFakeClient.InterfaceWrite(W: TJSONSerializer;
  48058. const aMethod: TServiceMethod; const aParamInfo: TServiceMethodArgument;
  48059. aParamValue: Pointer);
  48060. begin
  48061. W.Add(fClient.fClient.FakeCallbackRegister(fClient,aMethod,aParamInfo,aParamValue));
  48062. W.Add(',');
  48063. end;
  48064. destructor TInterfacedObjectFakeClient.Destroy;
  48065. begin
  48066. fClient.fClient.InternalLog('%(%).Destroy I%',
  48067. [ClassType,pointer(self),fClient.InterfaceURI],sllTrace);
  48068. inherited Destroy;
  48069. end;
  48070. constructor TInterfacedObjectFakeServer.Create(aRequest: TSQLRestServerURIContext;
  48071. aFactory: TInterfaceFactory; aFakeID: Integer);
  48072. var opt: TInterfacedObjectFromFactoryOptions;
  48073. begin
  48074. if aRequest.ClientKind=ckFramework then
  48075. opt := [ifoJsonAsExtended] else
  48076. opt := [];
  48077. fServer := aRequest.Server;
  48078. fService := aRequest.Service;
  48079. fLowLevelConnectionID := aRequest.Call^.LowLevelConnectionID;
  48080. fClientDrivenID := aFakeID;
  48081. inherited Create(aFactory,opt,CallbackInvoke,nil);
  48082. Get(fFakeInterface);
  48083. end;
  48084. destructor TInterfacedObjectFakeServer.Destroy;
  48085. begin
  48086. if fServer<>nil then begin // may be called asynchronously AFTER server is down
  48087. fServer.InternalLog('%(%:%).Destroy I%',
  48088. [ClassType,pointer(self),fClientDrivenID,fService.InterfaceURI],sllTrace);
  48089. if fServer.Services<>nil then
  48090. with (fServer.Services as TServiceContainerServer) do
  48091. if fFakeCallbacks<>nil then
  48092. FakeCallbackRemove(self);
  48093. end;
  48094. inherited Destroy;
  48095. end;
  48096. function TInterfacedObjectFakeServer.CallbackInvoke(const aMethod: TServiceMethod;
  48097. const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8;
  48098. aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean;
  48099. begin // here aClientDrivenID^ = FakeCall ID
  48100. if fServer=nil then begin
  48101. if aErrorMsg<>nil then
  48102. aErrorMsg^ := 'Server was already shutdown';
  48103. result := true;
  48104. exit;
  48105. end;
  48106. if not Assigned(fServer.OnNotifyCallback) then
  48107. raise EServiceException.CreateUTF8('% does not implement callbacks for I%',
  48108. [fServer,aMethod.InterfaceDotMethodName]);
  48109. if fReleasedOnClientSide then begin
  48110. if not IdemPropName(fFactory.fInterfaceTypeInfo^.Name,'ISynLogCallback') then
  48111. fServer.InternalLog('%.CallbackInvoke: % instance has been released on '+
  48112. 'the client side, so I% callback notification was NOT sent',
  48113. [self,fFactory.fInterfaceTypeInfo^.Name,aMethod.InterfaceDotMethodName],sllWarning);
  48114. if fRaiseExceptionOnInvokeError or
  48115. ((fServer.Services<>nil) and
  48116. (coRaiseExceptionIfReleasedByClient in
  48117. (fServer.Services as TServiceContainerServer).CallbackOptions)) then begin
  48118. if aErrorMsg<>nil then
  48119. aErrorMsg^ := FormatUTF8('%.CallbackInvoke(I%): instance has been '+
  48120. 'released on client side',[self,aMethod.InterfaceDotMethodName]);
  48121. result := false; // will raise an exception
  48122. end else
  48123. result := true; // do not raise an exception here: just log warning
  48124. end else begin
  48125. if aMethod.ArgsOutputValuesCount=0 then
  48126. aResult := nil; // no result -> asynchronous non blocking callback
  48127. result := fServer.OnNotifyCallback(fServer,aMethod.InterfaceDotMethodName,
  48128. aParams,fLowLevelConnectionID,aClientDrivenID^,aResult,aErrorMsg);
  48129. end;
  48130. end;
  48131. procedure TSQLRestServerURIContext.ExecuteCallback(var Par: PUTF8Char;
  48132. ParamInterfaceInfo: PTypeInfo; out Obj);
  48133. var FakeID: PtrInt;
  48134. factory: TInterfaceFactory;
  48135. instance: TInterfacedObjectFakeServer;
  48136. begin
  48137. if not Assigned(Server.OnNotifyCallback) then
  48138. raise EServiceException.CreateUTF8('% does not implement callbacks for I%',
  48139. [Server,ParamInterfaceInfo^.Name]);
  48140. FakeID := GetInteger(GetJSONField(Par,Par)); // GetInteger returns a PtrInt
  48141. if Par=nil then
  48142. Par := @NULL_SHORTSTRING; // allow e.g. '[12345]'
  48143. if (FakeID=0) or (ParamInterfaceInfo=TypeInfo(IInvokable)) then begin
  48144. pointer(Obj) := pointer(FakeID); // Obj = IInvokable(FakeID)
  48145. exit;
  48146. end;
  48147. factory := TInterfaceFactory.Get(ParamInterfaceInfo);
  48148. instance := TInterfacedObjectFakeServer.Create(self,factory,FakeID);
  48149. pointer(Obj) := instance.fFakeInterface;
  48150. (Server.Services as TServiceContainerServer).FakeCallbackAdd(instance);
  48151. end;
  48152. { TInterfacedObjectFromFactory }
  48153. constructor TInterfacedObjectFromFactory.Create(aFactory: TInterfaceFactory;
  48154. aOptions: TInterfacedObjectFromFactoryOptions; aInvoke: TOnFakeInstanceInvoke;
  48155. aNotifyDestroy: TOnFakeInstanceDestroy);
  48156. begin
  48157. inherited Create;
  48158. fFactory := aFactory;
  48159. fOptions := aOptions;
  48160. fInvoke := aInvoke;
  48161. fNotifyDestroy := aNotifyDestroy;
  48162. end;
  48163. destructor TInterfacedObjectFromFactory.Destroy;
  48164. var C: TClass;
  48165. begin
  48166. if Assigned(fNotifyDestroy) then
  48167. try // release server instance
  48168. fNotifyDestroy(fClientDrivenID);
  48169. except
  48170. on E: Exception do begin
  48171. C := E.ClassType;
  48172. if (C=EInterfaceStub) or (C=EInterfaceFactoryException) or
  48173. (C=EAccessViolation) {$ifndef LVCL}or (C=EInvalidPointer){$endif} then
  48174. raise; // ignore all low-level exceptions
  48175. end;
  48176. end;
  48177. inherited;
  48178. end;
  48179. { TInterfaceFactory }
  48180. function TypeInfoToMethodValueType(P: PTypeInfo): TServiceMethodValueType;
  48181. var IsObjCustomIndex: integer;
  48182. begin
  48183. result := smvNone;
  48184. if P<>nil then
  48185. case P^.Kind of
  48186. tkInteger:
  48187. case P^.OrdType of
  48188. otSLong: result := smvInteger;
  48189. otULong: result := smvCardinal;
  48190. end;
  48191. tkInt64{$ifdef FPC}, tkQWord{$endif}:
  48192. result := smvInt64;
  48193. {$ifdef FPC}
  48194. tkBool:
  48195. result := smvBoolean;
  48196. tkEnumeration:
  48197. result := smvEnum;
  48198. {$else}
  48199. tkEnumeration:
  48200. if P=TypeInfo(boolean) then
  48201. result := smvBoolean else
  48202. result := smvEnum;
  48203. {$endif}
  48204. tkSet:
  48205. result := smvSet;
  48206. tkFloat:
  48207. if P=TypeInfo(TDateTime) then
  48208. result := smvDateTime else
  48209. case P^.FloatType of
  48210. ftCurr: result := smvCurrency;
  48211. ftDoub: result := smvDouble;
  48212. end;
  48213. {$ifdef FPC}tkAString,{$endif} tkLString:
  48214. if P=TypeInfo(RawJSON) then
  48215. result := smvRawJSON else
  48216. if P=TypeInfo(RawByteString) then
  48217. result := smvRawByteString else
  48218. {$ifndef UNICODE}
  48219. if P=TypeInfo(AnsiString) then
  48220. result := smvString else
  48221. result := smvRawUTF8; // UTF-8 by default
  48222. {$ifdef HASVARUSTRING}
  48223. tkUString:
  48224. result := smvRawUTF8;
  48225. {$endif}
  48226. {$else UNICODE}
  48227. result := smvRawUTF8;
  48228. tkUString:
  48229. result := smvString;
  48230. {$endif UNICODE}
  48231. tkWString:
  48232. result := smvWideString;
  48233. tkClass:
  48234. with P^.ClassType^ do
  48235. if ClassHasPublishedFields(ClassType) or
  48236. (JSONObject(ClassType,IsObjCustomIndex,[cpRead,cpWrite]) in
  48237. [{$ifndef LVCL}oCollection,{$endif}oObjectList,oUtfs,oStrings,
  48238. oException,oCustom]) then
  48239. result := smvObject; // JSONToObject/ObjectToJSON types
  48240. {$ifdef FPC}tkObject,{$endif} tkRecord:
  48241. // Base64 encoding of our RecordLoad / RecordSave binary format
  48242. result := smvRecord;
  48243. {$ifndef NOVARIANTS}
  48244. tkVariant:
  48245. result := smvVariant;
  48246. {$endif}
  48247. tkDynArray: // TDynArray.LoadFromJSON / TTextWriter.AddDynArrayJSON type
  48248. result := smvDynArray;
  48249. tkInterface:
  48250. result := smvInterface;
  48251. tkUnknown: // assume var/out untyped arguments are in fact objects
  48252. result := smvObject;
  48253. end;
  48254. end;
  48255. var
  48256. InterfaceFactoryCache: TObjectListLocked;
  48257. procedure EnterInterfaceFactoryCache;
  48258. begin
  48259. if InterfaceFactoryCache=nil then
  48260. GarbageCollectorFreeAndNil(InterfaceFactoryCache,TObjectListLocked.Create);
  48261. InterfaceFactoryCache.Safe.Lock;
  48262. end;
  48263. class function TInterfaceFactory.Get(aInterface: PTypeInfo): TInterfaceFactory;
  48264. var i: integer;
  48265. F: ^TInterfaceFactory;
  48266. begin
  48267. if (aInterface=nil) or (aInterface^.Kind<>tkInterface) then
  48268. raise EInterfaceFactoryException.CreateUTF8('%.Get(nil)',[self]);
  48269. EnterInterfaceFactoryCache;
  48270. try
  48271. F := @InterfaceFactoryCache.List[0];
  48272. for i := 1 to InterfaceFactoryCache.Count do
  48273. if F^.fInterfaceTypeInfo=aInterface then begin
  48274. result := F^;
  48275. exit; // retrieved from cache
  48276. end else
  48277. inc(F);
  48278. // not existing -> create new instance from RTTI
  48279. {$ifdef HASINTERFACERTTI}
  48280. result := TInterfaceFactoryRTTI.Create(aInterface);
  48281. InterfaceFactoryCache.Add(result);
  48282. {$else}
  48283. result := nil; // make compiler happy
  48284. raise EInterfaceFactoryException.CreateUTF8('No RTTI available for I%: please '+
  48285. 'define the methods using a TInterfaceFactoryGenerated wrapper',[aInterface^.Name]);
  48286. {$endif}
  48287. finally
  48288. InterfaceFactoryCache.Safe.UnLock;
  48289. end;
  48290. end;
  48291. class procedure TInterfaceFactory.RegisterInterfaces(const aInterfaces: array of PTypeInfo);
  48292. {$ifdef HASINTERFACERTTI}
  48293. var i: integer;
  48294. begin
  48295. for i := 0 to high(aInterfaces) do
  48296. Get(aInterfaces[i]);
  48297. end;
  48298. {$else}
  48299. begin // in fact, TInterfaceFactoryGenerated.RegisterInterface() should do it
  48300. end;
  48301. {$endif}
  48302. class function TInterfaceFactory.Get(const aGUID: TGUID): TInterfaceFactory;
  48303. type TGUID32 = packed record a,b,c,d: integer; end; // brute force optimization
  48304. PGUID32 = ^TGUID32;
  48305. var i,ga: integer;
  48306. F: ^TInterfaceFactory;
  48307. GUID32: TGUID32 absolute aGUID;
  48308. begin
  48309. if InterfaceFactoryCache<>nil then begin
  48310. InterfaceFactoryCache.Safe.Lock;
  48311. F := @InterfaceFactoryCache.List[0];
  48312. ga := GUID32.a;
  48313. for i := 1 to InterfaceFactoryCache.Count do
  48314. with PGUID32(@F^.fInterfaceIID)^ do
  48315. if (a=ga) and (b=GUID32.b) and (c=GUID32.c) and (d=GUID32.d) then begin
  48316. result := F^;
  48317. InterfaceFactoryCache.Safe.UnLock;
  48318. exit;
  48319. end else
  48320. inc(F);
  48321. InterfaceFactoryCache.Safe.UnLock;
  48322. end;
  48323. result := nil;
  48324. end;
  48325. class procedure TInterfaceFactory.AddToObjArray(var Obj: TInterfaceFactoryObjArray;
  48326. const aGUIDs: array of TGUID);
  48327. var i: integer;
  48328. fac: TInterfaceFactory;
  48329. begin
  48330. for i := 0 to high(aGUIDs) do begin
  48331. fac := Get(aGUIDs[i]);
  48332. if fac<>nil then
  48333. ObjArrayAddOnce(Obj,fac);
  48334. end;
  48335. end;
  48336. class function TInterfaceFactory.GUID2TypeInfo(
  48337. const aGUIDs: array of TGUID): PTypeInfoDynArray;
  48338. var i: integer;
  48339. begin
  48340. SetLength(result,length(aGUIDs));
  48341. for i := 0 to high(aGUIDs) do
  48342. result[i] := GUID2TypeInfo(aGUIDs[i]);
  48343. end;
  48344. class function TInterfaceFactory.GUID2TypeInfo(const aGUID: TGUID): PTypeInfo;
  48345. var fact: TInterfaceFactory;
  48346. begin
  48347. fact := Get(aGUID);
  48348. if fact=nil then
  48349. raise EServiceException.CreateUTF8(
  48350. '%.GUID2TypeInfo(%): Interface not registered - use %.RegisterInterfaces()',
  48351. [self,GUIDToShort(aGUID),self]);
  48352. result := fact.fInterfaceTypeInfo;
  48353. end;
  48354. class function TInterfaceFactory.Get(const aInterfaceName: RawUTF8): TInterfaceFactory;
  48355. var L,i: integer;
  48356. begin
  48357. L := length(aInterfaceName);
  48358. if (InterfaceFactoryCache<>nil) and (L<>0) then
  48359. for i := 0 to InterfaceFactoryCache.Count-1 do begin
  48360. result := InterfaceFactoryCache.List[i];
  48361. if IdemPropName(result.fInterfaceTypeInfo^.Name,pointer(aInterfaceName),L) then
  48362. exit; // retrieved from cache
  48363. end;
  48364. result := nil;
  48365. end;
  48366. class function TInterfaceFactory.GetUsedInterfaces: TObjectList;
  48367. begin
  48368. result := InterfaceFactoryCache;
  48369. end;
  48370. constructor TInterfaceFactory.Create(aInterface: PTypeInfo);
  48371. var m,a,reg: integer;
  48372. WR: TTextWriter;
  48373. C: TClass;
  48374. ErrorMsg: RawUTF8;
  48375. {$ifdef HAS_FPREG}
  48376. ValueIsInFPR:boolean;
  48377. {$endif}
  48378. {$ifdef CPUX86}
  48379. offs: integer;
  48380. {$else}
  48381. {$ifdef Linux} // not used for Win64
  48382. fpreg: integer;
  48383. {$endif}
  48384. {$endif}
  48385. label error;
  48386. begin
  48387. if aInterface=nil then
  48388. raise EInterfaceFactoryException.CreateUTF8('%.Create(nil)',[self]);
  48389. if aInterface^.Kind<>tkInterface then
  48390. raise EInterfaceFactoryException.CreateUTF8(
  48391. '%.Create(%): % is not an interface',[self,aInterface^.Name,aInterface^.Name]);
  48392. {$ifndef NOVARIANTS}
  48393. fDocVariantOptions := JSON_OPTIONS_FAST;
  48394. {$endif}
  48395. {$ifdef CPUAARCH64}
  48396. fDetectX0ResultMagic := $AAAAAAAA; // alf: see comment above
  48397. {$endif}
  48398. fInterfaceTypeInfo := aInterface;
  48399. fInterfaceIID := aInterface^.InterfaceGUID^;
  48400. if IsNullGUID(fInterfaceIID) then
  48401. raise EInterfaceFactoryException.CreateUTF8(
  48402. '%.Create: % has no GUID',[self,aInterface^.Name]);
  48403. fInterfaceName := ToUTF8(fInterfaceTypeInfo^.Name);
  48404. // retrieve all interface methods (recursively including ancestors)
  48405. fMethod.InitSpecific(TypeInfo(TServiceMethodDynArray),fMethods,djRawUTF8,
  48406. @fMethodsCount,true);
  48407. AddMethodsFromTypeInfo(aInterface); // from RTTI or generated code
  48408. if fMethodsCount=0 then
  48409. raise EInterfaceFactoryException.CreateUTF8(
  48410. '%.Create(%): interface has no RTTI',[self,fInterfaceName]);
  48411. fMethodIndexCurrentFrameCallback := -1;
  48412. fMethodIndexCallbackReleased := -1;
  48413. SetLength(fMethods,fMethodsCount);
  48414. // compute additional information for each method
  48415. for m := 0 to fMethodsCount-1 do
  48416. with fMethods[m] do begin
  48417. InterfaceDotMethodName := fInterfaceName+'.'+URI;
  48418. if InterfaceDotMethodName[1] in ['I','i'] then
  48419. delete(InterfaceDotMethodName,1,1); // as in TServiceFactory.Create
  48420. IsInherited := HierarchyLevel<>fAddMethodsLevel;
  48421. ExecutionMethodIndex := m+RESERVED_VTABLE_SLOTS;
  48422. ArgsInFirst := -1;
  48423. ArgsInLast := -2;
  48424. ArgsOutFirst := -1;
  48425. ArgsOutLast := -2;
  48426. ArgsNotResultLast := -2;
  48427. ArgsOutNotResultLast := -2;
  48428. ArgsResultIndex := -1;
  48429. ArgsManagedFirst := -1;
  48430. ArgsManagedLast := -2;
  48431. Args[0].ValueType := smvSelf;
  48432. for a := 1 to high(Args) do
  48433. with Args[a] do begin
  48434. ValueType := TypeInfoToMethodValueType(ArgTypeInfo);
  48435. case ValueType of
  48436. smvNone: begin
  48437. case ArgTypeInfo^.Kind of
  48438. tkClass: begin
  48439. C := ArgTypeInfo^.ClassType^.ClassType;
  48440. if C.InheritsFrom(TList) then
  48441. ErrorMsg := ' - use TObjectList instead' else
  48442. {$ifndef LVCL}
  48443. if (C.InheritsFrom(TCollection) and not C.InheritsFrom(TInterfacedCollection)) and
  48444. (JSONSerializerRegisteredCollection.Find(TCollectionClass(C))=nil) then
  48445. ErrorMsg := ' - inherit from TInterfacedCollection '+
  48446. 'or use TJSONSerializer.RegisterCollectionForJSON()' else
  48447. {$endif}
  48448. ErrorMsg := ' - use TJSONSerializer.RegisterCustomSerializer()';
  48449. end;
  48450. tkInteger: ErrorMsg := ' - use integer/cardinal instead';
  48451. tkFloat: ErrorMsg := ' - use double/currency instead';
  48452. end;
  48453. error: raise EInterfaceFactoryException.CreateUTF8(
  48454. '%.Create: %.% "%" parameter has unexpected type %%',
  48455. [self,aInterface^.Name,URI,ParamName^,ArgTypeInfo^.Name,ErrorMsg]);
  48456. end;
  48457. smvObject:
  48458. if ValueDirection=smdResult then begin
  48459. ErrorMsg := ' - class not allowed as function result: use a var/out parameter';
  48460. goto error;
  48461. end;
  48462. smvInterface:
  48463. if ValueDirection in [smdVar,smdOut,smdResult] then begin
  48464. ErrorMsg := ' - interface not allowed as output: use a const parameter';
  48465. goto error;
  48466. end;
  48467. end;
  48468. if ValueDirection=smdResult then
  48469. ArgsResultIndex := a else begin
  48470. ArgsNotResultLast := a;
  48471. if ValueDirection<>smdOut then begin
  48472. inc(ArgsInputValuesCount);
  48473. if ArgsInFirst<0 then
  48474. ArgsInFirst := a;
  48475. ArgsInLast := a;
  48476. end;
  48477. if ValueDirection<>smdConst then
  48478. ArgsOutNotResultLast := a;
  48479. end;
  48480. if ValueDirection<>smdConst then begin
  48481. if ArgsOutFirst<0 then
  48482. ArgsOutFirst := a;
  48483. ArgsOutLast := a;
  48484. inc(ArgsOutputValuesCount);
  48485. end;
  48486. if ValueType in [smvObject,smvDynArray,smvRecord,smvInterface
  48487. {$ifndef NOVARIANTS},smvVariant{$endif}] then begin
  48488. if ArgsManagedFirst<0 then
  48489. ArgsManagedFirst := a;
  48490. ArgsManagedLast := a;
  48491. end;
  48492. end;
  48493. if ArgsOutputValuesCount=0 then // plain procedure with no out param
  48494. case ArgsInputValuesCount of
  48495. 1: if Args[1].ValueType=smvBoolean then
  48496. if IdemPropNameU(URI,'CurrentFrame') then
  48497. fMethodIndexCurrentFrameCallback := m;
  48498. 2: if (Args[1].ValueType=smvInterface) and
  48499. (Args[1].ArgTypeInfo=TypeInfo(IInvokable)) and
  48500. (Args[2].ValueType=smvRawUTF8) and
  48501. IdemPropNameU(URI,'CallbackReleased') then
  48502. fMethodIndexCallbackReleased := m;
  48503. end;
  48504. end;
  48505. // compute asm low-level layout of the parameters for each method
  48506. for m := 0 to fMethodsCount-1 do
  48507. with fMethods[m] do begin
  48508. // prepare stack and register layout
  48509. reg := PARAMREG_FIRST;
  48510. {$ifndef CPUX86}
  48511. {$ifdef Linux}
  48512. fpreg := FPREG_FIRST;
  48513. {$endif Linux}
  48514. {$endif CPUX86}
  48515. for a := 0 to high(Args) do
  48516. with Args[a] do begin
  48517. RegisterIdent := 0;
  48518. {$ifdef HAS_FPREG}
  48519. FPRegisterIdent := 0;
  48520. ValueIsInFPR := false;
  48521. {$endif}
  48522. ValueVar := CONST_ARGS_TO_VAR[ValueType];
  48523. IndexVar := ArgsUsedCount[ValueVar];
  48524. inc(ArgsUsedCount[ValueVar]);
  48525. include(ArgsUsed,ValueType);
  48526. if (ValueType in [smvRecord{$ifndef NOVARIANTS},smvVariant{$endif}
  48527. {$ifdef FPC},smvDynArray{$endif}]) or
  48528. (ValueDirection in [smdVar,smdOut]) or
  48529. ((ValueDirection=smdResult) and (ValueType in CONST_ARGS_RESULT_BY_REF)) then
  48530. Include(ValueKindAsm,vPassedByReference);
  48531. case ValueType of
  48532. smvRawUTF8..smvWideString:
  48533. Include(ValueKindAsm,vIsString);
  48534. smvDynArray:
  48535. if ObjArraySerializers.Find(ArgTypeInfo)<>nil then
  48536. Include(ValueKindAsm,vIsObjArray);
  48537. {$ifdef HAS_FPREG}
  48538. smvDouble,smvDateTime:
  48539. ValueIsInFPR := not (vPassedByReference in ValueKindAsm);
  48540. {$endif}
  48541. end;
  48542. case ValueType of
  48543. smvBoolean:
  48544. SizeInStorage := 1;
  48545. smvInteger, smvCardinal:
  48546. SizeInStorage := 4;
  48547. smvInt64, smvDouble, smvDateTime, smvCurrency:
  48548. SizeInStorage := 8;
  48549. smvEnum:
  48550. SizeInStorage := ArgTypeInfo^.EnumBaseType^.SizeInStorageAsEnum;
  48551. smvSet: begin
  48552. SizeInStorage := ArgTypeInfo^.SetEnumType^.SizeInStorageAsSet;
  48553. if SizeInStorage=0 then
  48554. raise EInterfaceFactoryException.CreateUTF8(
  48555. '%.Create: % set too big in %.% method % parameter',
  48556. [self,ArgTypeName^,fInterfaceTypeInfo^.Name,URI,ParamName^]);
  48557. end;
  48558. smvRecord:
  48559. if ArgTypeInfo^.RecordType^.Size<=PTRSIZ then
  48560. raise EInterfaceFactoryException.CreateUTF8(
  48561. '%.Create: % record too small in %.% method % parameter',
  48562. [self,ArgTypeName^,fInterfaceTypeInfo^.Name,URI,ParamName^]) else
  48563. SizeInStorage := PTRSIZ; // handle only records when passed by ref
  48564. else
  48565. SizeInStorage := PTRSIZ;
  48566. end;
  48567. if ValueDirection=smdResult then begin
  48568. if not(ValueType in CONST_ARGS_RESULT_BY_REF) then
  48569. continue; // ordinal/real/class results are returned in CPU/FPU registers
  48570. {$ifndef CPUX86}
  48571. InStackOffset := STACKOFFSET_NONE;
  48572. RegisterIdent := PARAMREG_RESULT;
  48573. continue;
  48574. {$endif CPUX86}
  48575. // CPUX86 would add an additional by-ref parameter
  48576. end;
  48577. {$ifdef CPU32}
  48578. if ValueDirection=smdConst then
  48579. SizeInStack := CONST_ARGS_IN_STACK_SIZE[ValueType] else
  48580. {$endif}
  48581. SizeInStack := PTRSIZ; // always aligned to 8 bytes boundaries for 64-bit
  48582. if{$ifndef CPUARM}
  48583. // on ARM, ordinals>PTRSIZ can also be placed in the normal registers !!
  48584. (SizeInStack<>PTRSIZ) or
  48585. {$endif CPUARM}
  48586. {$ifdef CPUX86}
  48587. (reg>PARAMREG_LAST) // Win32, Linux x86
  48588. {$else}
  48589. {$ifdef Linux} // Linux x64, arm, aarch64
  48590. ((ValueIsInFPR) and (fpreg>FPREG_LAST)) or
  48591. ((not ValueIsInFPR) and (reg>PARAMREG_LAST))
  48592. {$else}
  48593. (reg>PARAMREG_LAST) // Win64
  48594. {$endif Linux}
  48595. {$endif CPUX86}
  48596. // alf: TODO: fix smvDynArray as expected by fpc\compiler\i386\cpupara.pas
  48597. {$ifdef FPC}or ((ValueType in [smvRecord,smvDynArray]) and
  48598. not (vPassedByReference in ValueKindAsm)){$endif} then begin
  48599. // this parameter would go on the stack
  48600. InStackOffset := ArgsSizeInStack;
  48601. inc(ArgsSizeInStack,SizeInStack);
  48602. end else begin
  48603. // this parameter would go in a register
  48604. InStackOffset := STACKOFFSET_NONE;
  48605. {$ifndef CPUX86}
  48606. if (ArgsResultIndex>=0) and (reg=PARAMREG_RESULT) and
  48607. (Args[ArgsResultIndex].ValueType in CONST_ARGS_RESULT_BY_REF) then begin
  48608. inc(reg); // this register is reserved for method result pointer
  48609. end;
  48610. {$endif}
  48611. {$ifdef HAS_FPREG}
  48612. if ValueIsInFPR then begin
  48613. // put in a floating-point register
  48614. {$ifdef Linux}
  48615. FPRegisterIdent := fpreg;
  48616. inc(fpreg);
  48617. {$else}
  48618. FPRegisterIdent := reg; // Win64 ABI: reg and fpreg do overlap
  48619. inc(reg);
  48620. {$endif Linux}
  48621. end
  48622. else
  48623. {$endif} begin
  48624. // put in an integer register
  48625. {$ifdef CPUARM}
  48626. // on 32-bit ARM, ordinals>PTRSIZ are also placed in normal registers
  48627. if (SizeInStack>PTRSIZ) and ((reg and 1)=0) then
  48628. inc(reg); // must be aligned on even boundary
  48629. // check if we have still enough registers, after previous increments
  48630. if ((PARAMREG_LAST-reg+1)*PTRSIZ)<SizeInStack then begin
  48631. // no space, put on stack
  48632. InStackOffset := ArgsSizeInStack;
  48633. inc(ArgsSizeInStack,SizeInStack);
  48634. // all other parameters following the current one, must also be placed on stack
  48635. reg := PARAMREG_LAST+1;
  48636. continue;
  48637. end;
  48638. RegisterIdent := reg;
  48639. if SizeInStack>PTRSIZ then
  48640. inc(reg,SizeInStack shr PTRSHR) else
  48641. inc(reg);
  48642. {$else}
  48643. RegisterIdent := reg;
  48644. inc(reg);
  48645. {$endif CPUARM}
  48646. end;
  48647. end;
  48648. end;
  48649. if ArgsSizeInStack>MAX_EXECSTACK then
  48650. raise EInterfaceFactoryException.CreateUTF8(
  48651. '%.Create: Stack size % > % for %.% method',
  48652. [self,ArgsSizeInStack,MAX_EXECSTACK,fInterfaceTypeInfo^.Name,URI]);
  48653. {$ifdef CPUX86}
  48654. // pascal/register convention are passed left-to-right -> reverse order
  48655. offs := ArgsSizeInStack;
  48656. for a := 0 to high(Args) do
  48657. with Args[a] do
  48658. if InStackOffset>=0 then begin
  48659. dec(offs,SizeInStack);
  48660. InStackOffset := offs;
  48661. end;
  48662. //assert(offs=0);
  48663. {$endif CPUX86}
  48664. end;
  48665. WR := TJSONSerializer.CreateOwnedStream;
  48666. try
  48667. // compute the default results JSON array for all methods
  48668. for m := 0 to fMethodsCount-1 do
  48669. with fMethods[m] do begin
  48670. WR.CancelAll;
  48671. WR.Add('[');
  48672. for a := ArgsOutFirst to ArgsOutLast do
  48673. with Args[a] do
  48674. if ValueDirection in [smdVar,smdOut,smdResult] then
  48675. AddDefaultJSON(WR);
  48676. WR.CancelLastComma;
  48677. WR.Add(']');
  48678. WR.SetText(DefaultResult);
  48679. end;
  48680. // compute the service contract as a JSON array
  48681. WR.CancelAll;
  48682. WR.Add('[');
  48683. for m := 0 to fMethodsCount-1 do
  48684. with fMethods[m] do begin
  48685. WR.Add('{"method":"%","arguments":[',[URI]);
  48686. for a := 0 to High(Args) do
  48687. Args[a].SerializeToContract(WR);
  48688. WR.CancelLastComma;
  48689. WR.AddShort(']},');
  48690. end;
  48691. WR.CancelLastComma;
  48692. WR.Add(']');
  48693. WR.SetText(fContract);
  48694. finally
  48695. WR.Free;
  48696. end;
  48697. end;
  48698. function TInterfaceFactory.FindMethodIndex(const aMethodName: RawUTF8): integer;
  48699. begin
  48700. if (self=nil) or (aMethodName='') then
  48701. result := -1 else
  48702. if fMethodsCount<10 then begin
  48703. for result := 0 to fMethodsCount-1 do
  48704. if IdemPropNameU(fMethods[result].URI,aMethodName) then
  48705. exit;
  48706. result := -1;
  48707. end else
  48708. result := fMethod.FindHashed(aMethodName);
  48709. if (result<0) and (aMethodName[1]<>'_') then
  48710. result := FindMethodIndex('_'+aMethodName);
  48711. end;
  48712. function TInterfaceFactory.FindFullMethodIndex(const aFullMethodName: RawUTF8;
  48713. alsoSearchExactMethodName: boolean): integer;
  48714. begin
  48715. if PosEx('.',aFullMethodName)>=0 then
  48716. for result := 0 to fMethodsCount-1 do
  48717. if IdemPropNameU(fMethods[result].InterfaceDotMethodName,aFullMethodName) then
  48718. exit;
  48719. if alsoSearchExactMethodName then
  48720. result := FindMethodIndex(aFullMethodName) else
  48721. result := -1;
  48722. end;
  48723. function TInterfaceFactory.CheckMethodIndex(const aMethodName: RawUTF8): integer;
  48724. begin
  48725. if self=nil then
  48726. raise EInterfaceFactoryException.Create('TInterfaceFactory(nil).CheckMethodIndex');
  48727. result := FindMethodIndex(aMethodName);
  48728. if result<0 then
  48729. raise EInterfaceFactoryException.CreateUTF8(
  48730. '%.CheckMethodIndex: %.% not found',[self,fInterfaceTypeInfo^.Name,aMethodName]);
  48731. end;
  48732. function TInterfaceFactory.CheckMethodIndex(aMethodName: PUTF8Char): integer;
  48733. begin
  48734. result := CheckMethodIndex(RawUTF8(aMethodName));
  48735. end;
  48736. procedure TInterfaceFactory.CheckMethodIndexes(const aMethodName: array of RawUTF8;
  48737. aSetAllIfNone: boolean; out aBits: TInterfaceFactoryMethodBits);
  48738. var i: integer;
  48739. begin
  48740. if aSetAllIfNone and (high(aMethodName)<0) then begin
  48741. FillCharFast(aBits,sizeof(aBits),255);
  48742. exit;
  48743. end;
  48744. FillCharFast(aBits,sizeof(aBits),0);
  48745. for i := 0 to high(aMethodName) do
  48746. include(aBits,CheckMethodIndex(aMethodName[i]));
  48747. end;
  48748. function TInterfaceFactory.GetMethodName(MethodIndex: integer): RawUTF8;
  48749. begin
  48750. if (MethodIndex<0) or (self=nil) then
  48751. result := '' else
  48752. if MethodIndex<SERVICE_PSEUDO_METHOD_COUNT then
  48753. result := SERVICE_PSEUDO_METHOD[TServiceInternalMethod(MethodIndex)] else begin
  48754. dec(MethodIndex,SERVICE_PSEUDO_METHOD_COUNT);
  48755. if cardinal(MethodIndex)<fMethodsCount then
  48756. result := fMethods[MethodIndex].URI else
  48757. result := '';
  48758. end;
  48759. end;
  48760. function TInterfaceFactory.GetFullMethodName(aMethodIndex: integer): RawUTF8;
  48761. begin
  48762. if self=nil then
  48763. result := '' else begin
  48764. result := GetMethodName(aMethodIndex);
  48765. if result = '' then
  48766. result := fInterfaceName else
  48767. result := fInterfaceName+'.'+result;
  48768. end;
  48769. end;
  48770. { low-level ASM for TInterfaceFactory.GetMethodsVirtualTable
  48771. - all ARM, AARCH64 and Linux64 code below was provided by ALF! Thanks! :) }
  48772. {$ifdef FPC}
  48773. {$ifdef CPUARM}
  48774. procedure TInterfacedObjectFake.ArmFakeStub;
  48775. var smetndx: pointer;
  48776. sd7, sd6, sd5, sd4, sd3, sd2, sd1, sd0: double;
  48777. sr3,sr2,sr1,sr0: pointer;
  48778. asm
  48779. // get method index
  48780. str v1,smetndx
  48781. // store registers
  48782. vstr d0,sd0
  48783. vstr d1,sd1
  48784. vstr d2,sd2
  48785. vstr d3,sd3
  48786. vstr d4,sd4
  48787. vstr d5,sd5
  48788. vstr d6,sd6
  48789. vstr d7,sd7
  48790. str r0,sr0
  48791. str r1,sr1
  48792. str r2,sr2
  48793. str r3,sr3
  48794. // TFakeCallStack address as 2nd parameter
  48795. // there is no lea equivalent instruction for ARM (AFAIK), so this is calculated by hand (by looking at assembler)
  48796. sub r1, fp, #128
  48797. // branch to the FakeCall function
  48798. bl FakeCall
  48799. // FakeCall should set Int64 result in method result, and float in aCall.FPRegs["sd0"]
  48800. vstr d0,sd0
  48801. end;
  48802. {$endif}
  48803. {$ifdef CPUAARCH64}
  48804. procedure TInterfacedObjectFake.AArch64FakeStub;
  48805. var sx0, sx1, sx2, sx3, sx4, sx5, sx6, sx7: pointer;
  48806. sd0, sd1, sd2, sd3, sd4, sd5, sd6, sd7: double;
  48807. smetndx:pointer;
  48808. asm
  48809. // get method index
  48810. str x9,smetndx
  48811. // store registers
  48812. str d0,sd0
  48813. str d1,sd1
  48814. str d2,sd2
  48815. str d3,sd3
  48816. str d4,sd4
  48817. str d5,sd5
  48818. str d6,sd6
  48819. str d7,sd7
  48820. str x0,sx0
  48821. str x1,sx1
  48822. str x2,sx2
  48823. str x3,sx3
  48824. str x4,sx4
  48825. str x5,sx5
  48826. str x6,sx6
  48827. str x7,sx7
  48828. // TFakeCallStack address as 2nd parameter
  48829. // sx0 is at the stack pointer !
  48830. // local variables are stored in reverse on the stack
  48831. add x1, sp, #0
  48832. // branch to the FakeCall function
  48833. bl FakeCall
  48834. // FakeCall should set Int64 result in method result, and float in aCall.FPRegs["sd0"]
  48835. str d0,sd0
  48836. end;
  48837. {$endif}
  48838. {$endif}
  48839. {$ifdef CPUX64}
  48840. // note: x64 code below uses movlpd for reg,reg/mem,reg and movsd for reg,mem
  48841. procedure x64FakeStub;
  48842. var
  48843. smetndx,
  48844. {$ifdef Linux}
  48845. sxmm7, sxmm6, sxmm5, sxmm4,
  48846. {$endif}
  48847. sxmm3, sxmm2, sxmm1, sxmm0: pointer;
  48848. {$ifdef Linux}
  48849. sr9, sr8, srcx, srdx, srsi, srdi: pointer;
  48850. {$endif}
  48851. asm // mov ax,{MethodIndex}; jmp x64FakeStub
  48852. {$ifndef FPC}
  48853. // FakeCall(self: TInterfacedObjectFake; var aCall: TFakeCallStack): Int64
  48854. // So, make space for two variables (+shadow space)
  48855. // adds $50 to stack, so rcx .. at rpb+$10+$50 = rpb+$60
  48856. .params 2
  48857. {$endif}
  48858. and rax,$ffff
  48859. movlpd sxmm0,xmm0
  48860. movlpd sxmm1,xmm1
  48861. movlpd sxmm2,xmm2
  48862. movlpd sxmm3,xmm3
  48863. {$ifdef LINUX}
  48864. movlpd sxmm4,xmm4
  48865. movlpd sxmm5,xmm5
  48866. movlpd sxmm6,xmm6
  48867. movlpd sxmm7,xmm7
  48868. mov sr9,r9
  48869. mov sr8,r8
  48870. mov srcx,rcx
  48871. mov srdx,rdx
  48872. mov srsi,rsi
  48873. mov srdi,rdi
  48874. {$endif LINUX}
  48875. mov smetndx,rax
  48876. {$ifdef LINUX}
  48877. lea rsi, srdi // TFakeCallStack address as 2nd parameter
  48878. {$else}
  48879. {$ifndef FPC}
  48880. mov [rbp+$60],rcx
  48881. mov [rbp+$68],rdx
  48882. mov [rbp+$70],r8
  48883. mov [rbp+$78],r9
  48884. {$else}
  48885. mov [rbp+$10],rcx
  48886. mov [rbp+$18],rdx
  48887. mov [rbp+$20],r8
  48888. mov [rbp+$28],r9
  48889. {$endif FPC}
  48890. lea rdx, sxmm0 // TFakeCallStack address as 2nd parameter
  48891. {$endif LINUX}
  48892. call TInterfacedObjectFake.FakeCall
  48893. // FakeCall should set Int64 result in method result, and float in aCall.FPRegs["XMM0"]
  48894. movsd xmm0,sxmm0
  48895. end;
  48896. {$endif CPUX64}
  48897. const
  48898. STUB_SIZE = 65536; // 16*4 KB (4 KB = memory granularity)
  48899. {$ifdef FPC} // alf: multi platforms support
  48900. {$ifdef MSWINDOWS}
  48901. function AddrAllocMem(const Size, flProtect: DWORD): Pointer;
  48902. type
  48903. PMEMORY_BASIC_INFORMATION64 = ^_MEMORY_BASIC_INFORMATION64;
  48904. _MEMORY_BASIC_INFORMATION64 = record
  48905. BaseAddress: ULONGLONG;
  48906. AllocationBase: ULONGLONG;
  48907. AllocationProtect: DWORD;
  48908. __alignment1: DWORD;
  48909. RegionSize: ULONGLONG;
  48910. State: DWORD;
  48911. Protect: DWORD;
  48912. Type_: DWORD;
  48913. __alignment2: DWORD;
  48914. end;
  48915. var
  48916. mbiold: TMemoryBasicInformation;
  48917. {$ifdef CPUX64}
  48918. mbi: _MEMORY_BASIC_INFORMATION64 absolute mbiold;
  48919. {$else}
  48920. mbi: TMemoryBasicInformation;
  48921. {$endif}
  48922. Info: TSystemInfo;
  48923. P, Q: UInt64;
  48924. PP: Pointer;
  48925. error: DWORD;
  48926. Addr: UInt64;
  48927. begin
  48928. {$ifdef CPUX64}
  48929. Addr := UInt64(@x64FakeStub);
  48930. {$else}
  48931. Addr := 0;
  48932. {$endif}
  48933. result := nil;
  48934. if Addr = 0 then begin
  48935. result := VirtualAlloc(nil,Size,MEM_COMMIT,flProtect);
  48936. exit;
  48937. end;
  48938. P := UInt64(Addr);
  48939. Q := UInt64(Addr);
  48940. GetSystemInfo(Info);
  48941. // Interval = [2GB ..P.. 2GB] = 4GB
  48942. if Int64(P - (High(DWORD) div 2)) < 0 then
  48943. P := 1 else
  48944. P := UInt64(P - (High(DWORD) div 2)); // -2GB .
  48945. if UInt64(Q + (High(DWORD) div 2)) > High( {$IFDEF CPUX64}UInt64{$ELSE}UInt{$ENDIF} ) then
  48946. Q := High( {$IFDEF CPUX64}UInt64{$ELSE}UInt{$ENDIF} ) else
  48947. Q := Q + (High(DWORD) div 2); // + 2GB
  48948. while P < Q do begin
  48949. PP := Pointer(P);
  48950. if VirtualQuery(PP, mbiold, sizeof(_MEMORY_BASIC_INFORMATION64)) = 0 then
  48951. break;
  48952. if (mbi.State and MEM_FREE = MEM_FREE) and (UInt64(mbi.RegionSize) > Size) then
  48953. // this memory block is usable
  48954. if (UInt64(mbi.RegionSize) >= Info.dwAllocationGranularity) then begin
  48955. { The RegionSize must be greater than the dwAllocationGranularity }
  48956. { The address (PP) must be multiple of the allocation granularity (dwAllocationGranularity) . }
  48957. PP := Pointer(Info.dwAllocationGranularity *
  48958. (UInt64(PP) div Info.dwAllocationGranularity) +
  48959. Info.dwAllocationGranularity);
  48960. // if PP is multiple of dwAllocationGranularity then alloc memory
  48961. // if PP is not multiple of dwAllocationGranularity, VirtualAlloc will fail
  48962. if UInt64(PP) mod Info.dwAllocationGranularity=0 then
  48963. result := VirtualAlloc(PP, Size, MEM_COMMIT or MEM_RESERVE, flProtect);
  48964. if result <> nil then
  48965. exit;
  48966. end;
  48967. P := UInt64(mbi.BaseAddress) + UInt64(mbi.RegionSize); // Next region
  48968. end;
  48969. end;
  48970. {$else}
  48971. function AddrAllocMem(const Size, flProtect: DWORD): Pointer;
  48972. var P, Q: UInt64;
  48973. PP: Pointer;
  48974. Addr: UInt64;
  48975. begin
  48976. Addr := 0;
  48977. {$ifdef CPUX64}
  48978. Addr := UInt64(@x64FakeStub);
  48979. {$endif}
  48980. {$ifdef CPUARM}
  48981. Addr := UInt64(@TInterfacedObjectFake.ArmFakeStub);
  48982. {$endif}
  48983. {$ifdef CPUAARCH64}
  48984. Addr := UInt64(@TInterfacedObjectFake.AArch64FakeStub);
  48985. {$endif}
  48986. Result := nil;
  48987. if Addr = 0 then begin
  48988. Result := fpmmap(nil,STUB_SIZE,flProtect,MAP_PRIVATE OR MAP_ANONYMOUS,-1,0);
  48989. Exit;
  48990. end;
  48991. P := UInt64(Addr);
  48992. Q := UInt64(Addr);
  48993. { Interval = [2GB ..P.. 2GB] = 4GB }
  48994. if Int64(P - (High(DWORD) div 2)) < 0 then
  48995. P := 1 else
  48996. P := UInt64(P - (High(DWORD) div 2)); // -2GB .
  48997. if UInt64(Q + (High(DWORD) div 2)) > High( {$IFDEF CPU64}UInt64{$ELSE}DWORD{$ENDIF} ) then
  48998. Q := High( {$IFDEF CPU64}UInt64{$ELSE}DWORD{$ENDIF} ) else
  48999. Q := Q + (High(DWORD) div 2); // + 2GB
  49000. P := P AND $FFFFFFFFFFFF0000; //AND QWORD(-(STUB_SIZE-1));
  49001. Q := Q AND $FFFFFFFFFFFF0000;
  49002. while P < Q do begin
  49003. P := P + (STUB_SIZE);
  49004. PP := Pointer(P);
  49005. Result := fpmmap(PP,STUB_SIZE,flProtect,MAP_PRIVATE or MAP_ANONYMOUS,-1,0);
  49006. if (Result <> MAP_FAILED) then begin
  49007. {$ifdef CPUARM}
  49008. // are we close enough for a relative jump (24 bit signed)?
  49009. if ((PtrUInt(Result)-Addr)<DWORD($7FFFFF)) or (Addr-(PtrUInt(Result))<DWORD($7FFFFF)) then
  49010. exit else
  49011. fpmunmap(Result,STUB_SIZE);
  49012. {$else}
  49013. // are we close enough for a relative jump (32 bit signed)?
  49014. if ((PtrUInt(Result)-Addr)<Int64($7FFFFFFF)) or (Addr-(PtrUInt(Result))<Int64($7FFFFFFF)) then
  49015. exit else
  49016. fpmunmap(Result,STUB_SIZE);
  49017. {$endif}
  49018. end;
  49019. end;
  49020. end;
  49021. {$endif}
  49022. {$endif}
  49023. type
  49024. // internal memory buffer created with PAGE_EXECUTE_READWRITE flags
  49025. TFakeStubBuffer = class
  49026. protected
  49027. fStub: PByteArray;
  49028. fStubUsed: cardinal;
  49029. public
  49030. constructor Create;
  49031. destructor Destroy; override;
  49032. // call shall be protected by InterfaceFactoryCache critical section
  49033. class function Reserve(size: Cardinal): pointer;
  49034. end;
  49035. var
  49036. CurrentFakeStubBuffer: TFakeStubBuffer;
  49037. constructor TFakeStubBuffer.Create;
  49038. begin
  49039. {$ifdef MSWINDOWS}
  49040. {$ifdef FPC}
  49041. // alf: this is necessary, because a plain call to VirtualAlloc with FPC
  49042. // reserves a piece of memory too far away for a relative jump (on x64)
  49043. fStub := AddrAllocMem(STUB_SIZE,PAGE_EXECUTE_READWRITE);
  49044. {$else FPC}
  49045. fStub := VirtualAlloc(nil,STUB_SIZE,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
  49046. {$endif FPC}
  49047. {$else MSWINDOWS}
  49048. {$ifdef KYLIX3}
  49049. fStub := mmap(nil,STUB_SIZE,PROT_READ OR PROT_WRITE OR PROT_EXEC,MAP_PRIVATE OR MAP_ANONYMOUS,-1,0);
  49050. {$else}
  49051. fStub := AddrAllocMem(STUB_SIZE,PROT_READ OR PROT_WRITE OR PROT_EXEC);
  49052. {$endif}
  49053. {$endif MSWINDOWS}
  49054. end;
  49055. destructor TFakeStubBuffer.Destroy;
  49056. begin
  49057. {$ifdef MSWINDOWS}
  49058. VirtualFree(fStub,0,MEM_RELEASE);
  49059. {$else}
  49060. {$ifdef KYLIX3}
  49061. munmap(fStub,STUB_SIZE);
  49062. {$else}
  49063. fpmunmap(fStub,STUB_SIZE);
  49064. {$endif}
  49065. {$endif}
  49066. inherited;
  49067. end;
  49068. class function TFakeStubBuffer.Reserve(size: Cardinal): pointer;
  49069. begin
  49070. if size>STUB_SIZE then
  49071. raise EServiceException.CreateUTF8('%.Reserve(size=%>%)',[self,size,STUB_SIZE]);
  49072. if CurrentFakeStubBuffer=nil then
  49073. GarbageCollectorFreeAndNil(CurrentFakeStubBuffer,TFakeStubBuffer.Create) else
  49074. if CurrentFakeStubBuffer.fStubUsed+size>STUB_SIZE then begin
  49075. GarbageCollector.Add(CurrentFakeStubBuffer);
  49076. CurrentFakeStubBuffer := TFakeStubBuffer.Create;
  49077. end;
  49078. with CurrentFakeStubBuffer do begin
  49079. result := @fStub[fStubUsed];
  49080. inc(fStubUsed,size);
  49081. end;
  49082. end;
  49083. function TInterfaceFactory.GetMethodsVirtualTable: pointer;
  49084. var i, tmp: cardinal;
  49085. P: PCardinal;
  49086. begin
  49087. if fFakeVTable=nil then begin
  49088. InterfaceFactoryCache.Safe.Lock;
  49089. try
  49090. if fFakeVTable=nil then begin // avoid race condition error
  49091. SetLength(fFakeVTable,fMethodsCount+RESERVED_VTABLE_SLOTS);
  49092. fFakeVTable[0] := @TInterfacedObjectFake.FakeQueryInterface;
  49093. fFakeVTable[1] := @TInterfacedObjectFake.Fake_AddRef;
  49094. fFakeVTable[2] := @TInterfacedObjectFake.Fake_Release;
  49095. if fMethodsCount=0 then begin
  49096. result := pointer(fFakeVTable);
  49097. exit;
  49098. end;
  49099. tmp := {$ifdef CPUX86}fMethodsCount*24{$endif}
  49100. {$ifdef CPUX64}fMethodsCount*12{$endif}
  49101. {$ifdef CPUARM}fMethodsCount*12{$endif}
  49102. {$ifdef CPUAARCH64}($120 shr 2)+fMethodsCount*28{$endif};
  49103. fFakeStub := TFakeStubBuffer.Reserve(tmp);
  49104. PtrUInt(fFakeStub) := PtrUInt(fFakeStub){$ifdef CPUAARCH64} + $120{$endif};
  49105. P := pointer(fFakeStub);
  49106. for i := 0 to fMethodsCount-1 do begin
  49107. fFakeVTable[i+RESERVED_VTABLE_SLOTS] := P;
  49108. {$ifdef CPUX64}
  49109. P^ := $b866+(i shl 16); inc(P); // mov (r)ax,{MethodIndex}
  49110. PByte(P)^ := $e9; inc(PByte(P)); // jmp x64FakeStub
  49111. P^ := PtrUInt(@x64FakeStub)-PtrUInt(P)-4; inc(P);
  49112. P^ := $909090;
  49113. inc(PByte(P),3);
  49114. {$endif CPUX64}
  49115. {$ifdef CPUARM}
  49116. P^ := ($e3a040 shl 8)+i; inc(P); // mov r4 (v1),{MethodIndex} : store method index in register
  49117. tmp := ((PtrUInt(@TInterfacedObjectFake.ArmFakeStub)-PtrUInt(P)) shr 2)-2;
  49118. P^ := ($ea shl 24) + (tmp and $00FFFFFF); // branch ArmFakeStub (24bit relative, word aligned)
  49119. inc(P);
  49120. P^ := $e320f000; inc(P);
  49121. {$endif CPUARM}
  49122. {$ifdef CPUAARCH64}
  49123. // store method index in register x9
  49124. // $09 = r9 ... loop to $1F -> number shifted * $20
  49125. P^ := ($d280 shl 16)+(i shl 5)+$09; inc(P); // mov x9 ,{MethodIndex}
  49126. // we are using a register branch here
  49127. // fill register x10 with address
  49128. tmp := (PtrUInt(@TInterfacedObjectFake.AArch64FakeStub) shr 0) AND $FFFF;
  49129. P^ := ($d280 shl 16)+(tmp shl 5)+$0A; inc(P);
  49130. tmp := (PtrUInt(@TInterfacedObjectFake.AArch64FakeStub) shr 16) AND $FFFF;
  49131. P^ := ($f2a0 shl 16)+(tmp shl 5)+$0A; inc(P);
  49132. tmp := (PtrUInt(@TInterfacedObjectFake.AArch64FakeStub) shr 32) AND $FFFF;
  49133. P^ := ($f2c0 shl 16)+(tmp shl 5)+$0A; inc(P);
  49134. tmp := (PtrUInt(@TInterfacedObjectFake.AArch64FakeStub) shr 48) AND $FFFF;
  49135. P^ := ($f2e0 shl 16)+(tmp shl 5)+$0A; inc(P);
  49136. // branch to address in x10 register
  49137. P^ := ($d61f0140); inc(P);
  49138. P^ := $d503201f; inc(P);
  49139. {$endif CPUAARCH64}
  49140. {$ifdef CPUX86}
  49141. P^ := $68ec8b55; inc(P); // push ebp; mov ebp,esp
  49142. P^ := i; inc(P); // push {MethodIndex}
  49143. P^ := $e2895251; inc(P); // push ecx; push edx; mov edx,esp
  49144. PByte(P)^ := $e8; inc(PByte(P)); // call FakeCall
  49145. P^ := PtrUInt(@TInterfacedObjectFake.FakeCall)-PtrUInt(P)-4; inc(P);
  49146. P^ := $c25dec89; inc(P); // mov esp,ebp; pop ebp
  49147. P^ := fMethods[i].ArgsSizeInStack or $900000; // ret {StackSize}; nop
  49148. inc(PByte(P),3);
  49149. {$endif CPUX86}
  49150. end;
  49151. end;
  49152. finally
  49153. InterfaceFactoryCache.Safe.UnLock;
  49154. end;
  49155. end;
  49156. result := pointer(fFakeVTable);
  49157. end;
  49158. {$ifdef HASINTERFACERTTI} // see http://bugs.freepascal.org/view.php?id=26774
  49159. { TInterfaceFactoryRTTI }
  49160. procedure TInterfaceFactoryRTTI.AddMethodsFromTypeInfo(aInterface: PTypeInfo);
  49161. var P: Pointer;
  49162. PB: PByte absolute P;
  49163. PI: PInterfaceTypeData absolute P;
  49164. {$ifdef FPC}
  49165. PIR: PRawInterfaceTypeData absolute P;
  49166. {$endif}
  49167. PW: PWord absolute P;
  49168. PS: PShortString absolute P;
  49169. PME: ^TIntfMethodEntryTail absolute P;
  49170. PF: ^TParamFlags absolute P;
  49171. PP: ^PPTypeInfo absolute P;
  49172. Ancestor: PTypeInfo;
  49173. {$ifdef FPC}
  49174. propCount: integer;
  49175. aResultType: PTypeInfo;
  49176. {$else}
  49177. Kind: TMethodKind;
  49178. {$endif}
  49179. f: TParamFlags;
  49180. m,a: integer;
  49181. n: cardinal;
  49182. aURI: RawUTF8;
  49183. procedure RaiseError(const Format: RawUTF8; const Args: array of const);
  49184. begin
  49185. raise EInterfaceFactoryException.CreateUTF8(
  49186. '%.AddMethodsFromTypeInfo(%.%) failed - %',
  49187. [self,fInterfaceName,aURI,FormatUTF8(Format,Args)]);
  49188. end;
  49189. begin
  49190. // handle interface inheritance via recursive calls
  49191. P := aInterface^.ClassType;
  49192. if PI^.IntfParent<>nil then
  49193. Ancestor := Deref(PI^.IntfParent) else
  49194. Ancestor := nil;
  49195. if Ancestor<>nil then begin
  49196. AddMethodsFromTypeInfo(Ancestor);
  49197. inc(fAddMethodsLevel);
  49198. end;
  49199. // retrieve methods for this interface level
  49200. {$ifdef FPC}
  49201. if PI^.IntfUnit='System' then
  49202. exit;
  49203. if aInterface^.Kind=tkInterface then
  49204. P := AlignToPtr(@PI^.IntfUnit[ord(PI^.IntfUnit[0])+1]) else
  49205. P := AlignToPtr(@PIR^.IIDStr[ord(PIR^.IIDStr[0])+1]);
  49206. propCount := PSmallInt(P)^; // FPC add property information -> ignore now
  49207. inc(P,sizeOf(SmallInt));
  49208. P := AlignToPtr(P);
  49209. for a := 0 to propCount-1 do
  49210. P := AlignToPtr(@PPropInfo(P)^.Name[ord(PPropInfo(P)^.Name[0])+1]);
  49211. {$else}
  49212. P := AlignToPtr(@PI^.IntfUnit[ord(PI^.IntfUnit[0])+1]);
  49213. {$endif}
  49214. n := PW^; inc(PW);
  49215. if (PW^=$ffff) or (n=0) then
  49216. exit; // no RTTI or no method at this level of interface
  49217. inc(PW);
  49218. p := aligntoptr(p);
  49219. for m := fMethodsCount to fMethodsCount+n-1 do begin
  49220. // retrieve method name, and add to the methods list (with hashing)
  49221. SetString(aURI,PAnsiChar(@PS^[1]),ord(PS^[0]));
  49222. with PServiceMethod(fMethod.AddUniqueName(aURI,
  49223. '%.% method: duplicated name for %',[fInterfaceTypeInfo^.Name,aURI,self]))^ do begin
  49224. HierarchyLevel := fAddMethodsLevel;
  49225. {$ifdef FPC} // FPC has its own RTTI layout only since late 3.x
  49226. inc(PB,ord(PS^[0])+1);
  49227. inc(PB); // skip Version field (always 3)
  49228. {$ifdef CPUINTEL}
  49229. if PCallingConvention(P)^<>ccRegister then
  49230. RaiseError('method shall use register calling convention',[]);
  49231. {$endif CPUINTEL}
  49232. inc(PB,sizeOf(TCallingConvention));
  49233. P := AlignToPtr(P);// new Alignment
  49234. aResultType := DeRef(PP^);
  49235. inc(PP);
  49236. inc(PW); // skip StackSize
  49237. n := PB^;
  49238. inc(PB);
  49239. P := AlignToPtr(P);// new Alignment
  49240. if aResultType<>nil then // we have a function
  49241. SetLength(Args,n+1) else
  49242. SetLength(Args,n);
  49243. if length(Args)>MAX_METHOD_ARGS then
  49244. RaiseError('method has too many parameters: %>%',[Length(Args),MAX_METHOD_ARGS]);
  49245. if aResultType<>nil then
  49246. with Args[n] do begin
  49247. ParamName := @CONST_PSEUDO_RESULT_NAME;
  49248. ValueDirection := smdResult;
  49249. ArgTypeInfo := aResultType;
  49250. if ArgTypeInfo=TypeInfo(Integer) then // under FPC integer->'longint'
  49251. ArgTypeName := @CONST_INTEGER_NAME else
  49252. ArgTypeName := @ArgTypeInfo^.Name;
  49253. end;
  49254. for a := 0 to n-1 do
  49255. with Args[a],PVmtMethodParam(P)^ do begin
  49256. f := mORMot.TParamFlags(Flags);
  49257. if pfVar in f then
  49258. ValueDirection := smdVar else
  49259. if pfOut in f then
  49260. ValueDirection := smdOut;
  49261. ArgsNotResultLast := a;
  49262. if ValueDirection<>smdConst then
  49263. ArgsOutNotResultLast := a;
  49264. ArgTypeInfo := mORMot.PTypeInfo(Deref(mORMot.PPTypeInfo(ParamType)));
  49265. ArgTypeName := @ArgTypeInfo^.Name;
  49266. if a>0 then
  49267. case TypeInfoToMethodValueType(ArgTypeInfo) of
  49268. smvRecord,smvDynArray:
  49269. if f*[pfConst,pfVar,pfOut{$IFDEF FPC_HAS_CONSTREF},pfConstRef{$endif}]=[] then
  49270. RaiseError('%: % parameter should be declared as const, var or out',
  49271. [ParamName^,ArgTypeName^]);
  49272. smvInterface:
  49273. if not (pfConst in f) then
  49274. RaiseError('%: % parameter should be declared as const',
  49275. [ParamName^,ArgTypeName^]);
  49276. end;
  49277. if Name='$self' then
  49278. ParamName := @CONST_PSEUDO_SELF_NAME else
  49279. ParamName := @Name;
  49280. P := AlignToPtr(@Name[ord(Name[0])+1]);
  49281. end;
  49282. {$else FPC} // Delphi code
  49283. PS := AlignToPtr(@PS^[ord(PS^[0])+1]);
  49284. Kind := PME^.Kind;
  49285. if PME^.CC<>ccRegister then
  49286. RaiseError('method shall use register calling convention',[]);
  49287. // retrieve method call arguments from RTTI
  49288. n := PME^.ParamCount;
  49289. inc(PME);
  49290. if Kind=mkFunction then
  49291. SetLength(Args,n+1) else
  49292. SetLength(Args,n);
  49293. if length(Args)>MAX_METHOD_ARGS then
  49294. RaiseError('method has too many parameters: %>%',[Length(Args),MAX_METHOD_ARGS]);
  49295. for a := 0 to n-1 do
  49296. with Args[a] do begin
  49297. f := PF^;
  49298. inc(PF);
  49299. if pfVar in f then
  49300. ValueDirection := smdVar else
  49301. if pfOut in f then
  49302. ValueDirection := smdOut;
  49303. ArgsNotResultLast := a;
  49304. if ValueDirection<>smdConst then
  49305. ArgsOutNotResultLast := a;
  49306. ParamName := PS;
  49307. PS := AlignToPtr(@PS^[ord(PS^[0])+1]);
  49308. SetFromRTTI(PB);
  49309. {$ifdef ISDELPHIXE}
  49310. inc(PB,PW^); // skip custom attributes
  49311. {$endif}
  49312. if a>0 then
  49313. case TypeInfoToMethodValueType(ArgTypeInfo) of
  49314. smvRecord,smvDynArray:
  49315. if f*[pfConst,pfVar,pfOut]=[] then
  49316. RaiseError('%: % parameter should be declared as const, var or out',
  49317. [ParamName^,ArgTypeName^]);
  49318. smvInterface:
  49319. if not (pfConst in f) then
  49320. RaiseError('%: % parameter should be declared as const',
  49321. [ParamName^,ArgTypeName^]);
  49322. end;
  49323. end;
  49324. // add a pseudo argument after all arguments for functions
  49325. if Kind=mkFunction then
  49326. with Args[n] do begin
  49327. ParamName := @CONST_PSEUDO_RESULT_NAME;
  49328. ValueDirection := smdResult;
  49329. SetFromRTTI(PB);
  49330. end;
  49331. {$ifdef ISDELPHIXE}
  49332. inc(PB,PW^); // skip custom attributes
  49333. {$endif}
  49334. {$endif FPC}
  49335. // go to next method
  49336. end;
  49337. end;
  49338. end;
  49339. {$endif HASINTERFACERTTI} // see http://bugs.freepascal.org/view.php?id=26774
  49340. { TInterfaceFactoryGenerated }
  49341. procedure TInterfaceFactoryGenerated.AddMethod(
  49342. const aName: RawUTF8; const aParams: array of const);
  49343. const ARGPERARG = 3; // [ 0,'n1',TypeInfo(Integer), ... ]
  49344. var meth: PServiceMethod;
  49345. arg: ^TServiceMethodArgument;
  49346. na,ns,a: integer;
  49347. u: RawUTF8;
  49348. begin
  49349. if Length(aParams) mod ARGPERARG<>0 then
  49350. raise EInterfaceFactoryException.CreateUTF8(
  49351. '%: invalid aParams count for %.AddMethod("%")',[fInterfaceName,self,aName]);
  49352. meth := fMethod.AddUniqueName(aName,'%.% method: duplicated generated name for %',
  49353. [fInterfaceName,aName,self]);
  49354. na := length(aParams) div ARGPERARG;
  49355. SetLength(meth^.Args,na+1); // leave Args[0]=self
  49356. with meth^.Args[0] do begin
  49357. ParamName := @CONST_PSEUDO_SELF_NAME;
  49358. ArgTypeInfo := fInterfaceTypeInfo;
  49359. ArgTypeName := @ArgTypeInfo^.Name;
  49360. end;
  49361. ns := length(fTempStrings);
  49362. SetLength(fTempStrings,ns+na);
  49363. for a := 0 to na-1 do begin
  49364. arg := @meth^.Args[a+1];
  49365. if aParams[a*ARGPERARG].VType<>vtInteger then
  49366. raise EInterfaceFactoryException.CreateUTF8('%: invalid param type #% for %.AddMethod("%")',
  49367. [fInterfaceTypeInfo^.Name,a,self,aName]);
  49368. arg^.ValueDirection := TServiceMethodValueDirection(aParams[a*ARGPERARG].VInteger);
  49369. VarRecToUTF8(aParams[a*ARGPERARG+1],u);
  49370. if u='' then
  49371. raise EInterfaceFactoryException.CreateUTF8('%: invalid param name #% for %.AddMethod("%")',
  49372. [fInterfaceTypeInfo^.Name,a,self,aName]);
  49373. insert(AnsiChar(Length(u)),u,1); // create fake PShortString
  49374. arg^.ParamName := pointer(u);
  49375. fTempStrings[ns+a] := u;
  49376. if aParams[a*ARGPERARG+2].VType<>vtPointer then
  49377. raise EInterfaceFactoryException.CreateUTF8('%: expect TypeInfo() at #% for %.AddMethod("%")',
  49378. [fInterfaceTypeInfo^.Name,a,self,aName]);
  49379. arg^.ArgTypeInfo := aParams[a*ARGPERARG+2].VPointer;
  49380. {$ifdef FPC} // under FPC, TypeInfo(Integer)=TypeInfo(Longint)
  49381. if arg^.ArgTypeInfo=TypeInfo(Integer) then
  49382. arg^.ArgTypeName := @CONST_INTEGER_NAME else
  49383. {$endif}
  49384. arg^.ArgTypeName := @arg^.ArgTypeInfo^.Name;
  49385. end;
  49386. end;
  49387. class procedure TInterfaceFactoryGenerated.RegisterInterface(aInterface: PTypeInfo);
  49388. var i: integer;
  49389. begin
  49390. if (aInterface=nil) or (self=TInterfaceFactoryGenerated) then
  49391. raise EInterfaceFactoryException.CreateUTF8('%.RegisterInterface(nil)',[self]);
  49392. EnterInterfaceFactoryCache;
  49393. try
  49394. for i := 0 to InterfaceFactoryCache.Count-1 do
  49395. if TInterfaceFactory(InterfaceFactoryCache.List[i]).fInterfaceTypeInfo=aInterface then
  49396. raise EInterfaceFactoryException.CreateUTF8('Duplicated %.RegisterInterface(%)',
  49397. [self,aInterface^.Name]);
  49398. InterfaceFactoryCache.Add(Create(aInterface));
  49399. finally
  49400. InterfaceFactoryCache.Safe.UnLock;
  49401. end;
  49402. end;
  49403. { TInterfaceStubRules }
  49404. function TInterfaceStubRules.FindRuleIndex(const aParams: RawUTF8): integer;
  49405. begin
  49406. for result := 0 to length(Rules)-1 do
  49407. if Rules[result].Params=aParams then
  49408. exit;
  49409. result := -1;
  49410. end;
  49411. function TInterfaceStubRules.FindStrongRuleIndex(const aParams: RawUTF8): integer;
  49412. begin
  49413. for result := 0 to length(Rules)-1 do
  49414. if (Rules[result].Kind<>isUndefined) and (Rules[result].Params=aParams) then
  49415. exit;
  49416. result := -1;
  49417. end;
  49418. procedure TInterfaceStubRules.AddRule(Sender: TInterfaceStub;
  49419. aKind: TInterfaceStubRuleKind; const aParams, aValues: RawUTF8;
  49420. const aEvent: TNotifyEvent; aExceptionClass: ExceptClass;
  49421. aExpectedPassCountOperator: TSQLQueryOperator; aValue: cardinal);
  49422. var n,ndx: integer;
  49423. begin
  49424. ndx := FindRuleIndex(aParams);
  49425. n := length(Rules);
  49426. if ndx<0 then
  49427. SetLength(Rules,n+1) else
  49428. n := ndx;
  49429. if (aParams='') and (aKind<>isUndefined) then
  49430. DefaultRule := n;
  49431. with Rules[n] do begin
  49432. Params := aParams;
  49433. case aKind of
  49434. isUndefined:
  49435. ; // do not overwrite Values for weak rules like ExpectsCount/ExpectsTrace
  49436. isReturns:
  49437. Values := '['+AValues+']';
  49438. isFails:
  49439. Values := RawUTF8(Sender.ClassName)+' returned error: '+aValues;
  49440. else
  49441. Values := aValues;
  49442. end;
  49443. if aKind=isUndefined then
  49444. if aExpectedPassCountOperator=qoContains then
  49445. ExpectedTraceHash := aValue else begin
  49446. ExpectedPassCountOperator := aExpectedPassCountOperator;
  49447. ExpectedPassCount := aValue;
  49448. end else begin
  49449. Kind := aKind;
  49450. Execute := TMethod(aEvent);
  49451. ExceptionClass := aExceptionClass;
  49452. end;
  49453. end;
  49454. end;
  49455. { TInterfaceStub }
  49456. constructor EInterfaceStub.Create(Sender: TInterfaceStub;
  49457. const Method: TServiceMethod; const Error: RawUTF8);
  49458. begin
  49459. inherited CreateUTF8('Error in % for %.% - %',
  49460. [Sender,Sender.fInterface.fInterfaceName,Method.URI,Error]);
  49461. end;
  49462. constructor EInterfaceStub.Create(Sender: TInterfaceStub;
  49463. const Method: TServiceMethod; const Format: RawUTF8; const Args: array of const);
  49464. begin
  49465. Create(Sender,Method,FormatUTF8(Format,Args));
  49466. end;
  49467. function TInterfaceStubLog.Results: RawUTF8;
  49468. begin
  49469. if CustomResults='' then
  49470. result := Method^.DefaultResult else
  49471. result := CustomResults;
  49472. end;
  49473. procedure TInterfaceStubLog.AddAsText(WR: TTextWriter; aScope: TInterfaceStubLogLayouts;
  49474. SepChar: AnsiChar=',');
  49475. begin
  49476. if wName in aScope then
  49477. WR.AddString(Method^.URI);
  49478. if wParams in aScope then begin
  49479. WR.Add('(');
  49480. WR.AddString(Params);
  49481. WR.Add(')');
  49482. end;
  49483. if WasError then begin
  49484. WR.AddShort(' error "');
  49485. WR.AddString(CustomResults);
  49486. WR.Add('"');
  49487. end else
  49488. if (wResults in aScope) and (Method^.ArgsResultIndex>=0) then begin
  49489. if (wName in aScope) or (wParams in aScope) then
  49490. WR.Add('=');
  49491. if CustomResults='' then
  49492. WR.AddString(Method^.DefaultResult) else
  49493. WR.AddString(CustomResults);
  49494. end;
  49495. WR.Add(SepChar);
  49496. end;
  49497. constructor TOnInterfaceStubExecuteParamsAbstract.Create(aSender: TInterfaceStub;
  49498. aMethod: PServiceMethod; const aParams,aEventParams: RawUTF8);
  49499. begin
  49500. fSender := aSender;
  49501. fMethod := aMethod;
  49502. fParams := aParams;
  49503. fEventParams := aEventParams;
  49504. end;
  49505. procedure TOnInterfaceStubExecuteParamsAbstract.Error(
  49506. const Format: RawUTF8; const Args: array of const);
  49507. begin
  49508. Error(FormatUTF8(Format,Args));
  49509. end;
  49510. procedure TOnInterfaceStubExecuteParamsAbstract.Error(const aErrorMessage: RawUTF8);
  49511. begin
  49512. fFailed := true;
  49513. fResult := aErrorMessage;
  49514. end;
  49515. function TOnInterfaceStubExecuteParamsAbstract.GetSenderAsMockTestCase: TSynTestCase;
  49516. begin
  49517. result := (fSender as TInterfaceMock).TestCase;
  49518. end;
  49519. procedure TOnInterfaceStubExecuteParamsJSON.Returns(const Values: array of const);
  49520. begin
  49521. JSONEncodeArrayOfConst(Values,false,fResult);
  49522. end;
  49523. procedure TOnInterfaceStubExecuteParamsJSON.Returns(const ValuesJsonArray: RawUTF8);
  49524. begin
  49525. fResult := ValuesJsonArray;
  49526. end;
  49527. {$ifndef NOVARIANTS}
  49528. constructor TOnInterfaceStubExecuteParamsVariant.Create(aSender: TInterfaceStub;
  49529. aMethod: PServiceMethod; const aParams, aEventParams: RawUTF8);
  49530. var i: integer;
  49531. P: PUTF8Char;
  49532. tmp: TSynTempBuffer;
  49533. begin
  49534. inherited;
  49535. SetLength(fInput,fMethod^.ArgsInputValuesCount);
  49536. tmp.Init(aParams);
  49537. try
  49538. P := tmp.buf;
  49539. for i := 0 to fMethod^.ArgsInputValuesCount-1 do
  49540. P := VariantLoadJSON(fInput[i],P,nil,@aSender.fInterface.DocVariantOptions);
  49541. finally
  49542. tmp.Done;
  49543. end;
  49544. SetLength(fOutput,fMethod^.ArgsOutputValuesCount);
  49545. end;
  49546. function TOnInterfaceStubExecuteParamsVariant.GetInput(Index: Integer): variant;
  49547. begin
  49548. if cardinal(Index)>=fMethod^.ArgsInputValuesCount then
  49549. raise EInterfaceStub.Create(fSender,fMethod^,'Input[%>=%]',
  49550. [Index,fMethod^.ArgsInputValuesCount]) else
  49551. result := fInput[Index];
  49552. end;
  49553. procedure TOnInterfaceStubExecuteParamsVariant.SetOutput(Index: Integer;
  49554. const Value: variant);
  49555. begin
  49556. if cardinal(Index)>=fMethod^.ArgsOutputValuesCount then
  49557. raise EInterfaceStub.Create(fSender,fMethod^,'Output[%>=%]',
  49558. [Index,fMethod^.ArgsOutputValuesCount]) else
  49559. fOutput[Index] := Value;
  49560. end;
  49561. function TOnInterfaceStubExecuteParamsVariant.GetInNamed(const aParamName: RawUTF8): variant;
  49562. var L,a,ndx: integer;
  49563. begin
  49564. L := Length(aParamName);
  49565. ndx := 0;
  49566. if (L>0) and (fInput<>nil) then
  49567. for a := fMethod^.ArgsInFirst to fMethod^.ArgsInLast do
  49568. with fMethod^.Args[a] do
  49569. if ValueDirection in [smdConst,smdVar] then begin
  49570. if IdemPropName(ParamName^,pointer(aParamName),L) then begin
  49571. result := fInput[ndx];
  49572. exit;
  49573. end;
  49574. inc(ndx);
  49575. if cardinal(ndx)>=cardinal(fMethod^.ArgsInputValuesCount) then
  49576. break;
  49577. end;
  49578. raise EInterfaceStub.Create(fSender,fMethod^,'unknown input parameter "%"',[aParamName]);
  49579. end;
  49580. procedure TOnInterfaceStubExecuteParamsVariant.SetOutNamed(const aParamName: RawUTF8;
  49581. const Value: variant);
  49582. var L,a,ndx: integer;
  49583. begin
  49584. L := Length(aParamName);
  49585. ndx := 0;
  49586. if (L>0) and (fOutput<>nil) then
  49587. for a := fMethod^.ArgsOutFirst to fMethod^.ArgsOutLast do
  49588. with fMethod^.Args[a] do
  49589. if ValueDirection<>smdConst then begin
  49590. if IdemPropName(ParamName^,pointer(aParamName),L) then begin
  49591. fOutput[ndx] := Value;
  49592. exit;
  49593. end;
  49594. inc(ndx);
  49595. if cardinal(ndx)>=cardinal(fMethod^.ArgsOutputValuesCount) then
  49596. break;
  49597. end;
  49598. raise EInterfaceStub.Create(fSender,fMethod^,'unknown output parameter "%"',[aParamName]);
  49599. end;
  49600. procedure TOnInterfaceStubExecuteParamsVariant.SetResultFromOutput;
  49601. var a,ndx: integer;
  49602. W: TJSONSerializer;
  49603. begin
  49604. fResult := '';
  49605. if fOutput=nil then
  49606. exit;
  49607. W := TJSONSerializer.CreateOwnedStream;
  49608. try
  49609. W.Add('[');
  49610. ndx := 0;
  49611. for a := fMethod^.ArgsOutFirst to fMethod^.ArgsOutLast do
  49612. with fMethod^.Args[a] do
  49613. if ValueDirection<>smdConst then begin
  49614. if TVarData(fOutput[ndx]).VType=varEmpty then
  49615. AddDefaultJSON(W) else begin
  49616. W.AddVariant(fOutput[ndx],twJSONEscape);
  49617. W.Add(',');
  49618. end;
  49619. inc(ndx);
  49620. if cardinal(ndx)>=cardinal(fMethod^.ArgsOutputValuesCount) then
  49621. break;
  49622. end;
  49623. W.CancelLastComma;
  49624. W.Add(']');
  49625. W.SetText(fResult);
  49626. finally
  49627. W.Free;
  49628. end;
  49629. end;
  49630. function TOnInterfaceStubExecuteParamsVariant.InputAsDocVariant(
  49631. Kind: TServiceMethodParamsDocVariantKind; Options: TDocVariantOptions): variant;
  49632. begin
  49633. VarClear(result);
  49634. fMethod^.ArgsValuesAsDocVariant(Kind,TDocVariantData(Result),fInput,true,Options);
  49635. end;
  49636. function TOnInterfaceStubExecuteParamsVariant.OutputAsDocVariant(
  49637. Kind: TServiceMethodParamsDocVariantKind; Options: TDocVariantOptions): variant;
  49638. begin
  49639. VarClear(result);
  49640. fMethod^.ArgsValuesAsDocVariant(Kind,TDocVariantData(Result),fOutput,false,Options);
  49641. end;
  49642. {$endif NOVARIANTS}
  49643. constructor TInterfaceStub.Create(aFactory: TInterfaceFactory;
  49644. const aInterfaceName: RawUTF8);
  49645. var i: integer;
  49646. begin
  49647. if aFactory=nil then
  49648. raise EInterfaceStub.CreateUTF8(
  49649. '%.Create(%): Interface not registered - you could use '+
  49650. 'TInterfaceFactory.RegisterInterfaces()',[self,aInterfaceName]);
  49651. fInterface := aFactory;
  49652. SetLength(fRules,fInterface.MethodsCount);
  49653. for i := 0 to fInterface.MethodsCount-1 do
  49654. fRules[i].DefaultRule := -1;
  49655. fLog.Init(TypeInfo(TInterfaceStubLogDynArray),fLogs,@fLogCount);
  49656. end;
  49657. procedure TInterfaceStub.InternalGetInstance(out aStubbedInterface);
  49658. var fake: TInterfacedObjectFake;
  49659. begin
  49660. fake := TInterfacedObjectFake.Create(fInterface,[ifoJsonAsExtended],Invoke,InstanceDestroyed);
  49661. pointer(aStubbedInterface) := @fake.fVTable;
  49662. fake._AddRef;
  49663. fLastInterfacedObjectFake := fake;
  49664. end;
  49665. function TInterfaceStub.InternalCheck(aValid,aExpectationFailed: boolean;
  49666. const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean;
  49667. begin
  49668. result := aValid;
  49669. if aExpectationFailed and not aValid then
  49670. raise EInterfaceStub.CreateUTF8('%.InternalCheck(%) failed: %',
  49671. [self,fInterface.fInterfaceName,FormatUTF8(aErrorMsgFmt,aErrorMsgArgs)]);
  49672. end;
  49673. constructor TInterfaceStub.Create(const aInterfaceName: RawUTF8; out aStubbedInterface);
  49674. begin
  49675. Create(TInterfaceFactory.Get(aInterfaceName),aInterfaceName);
  49676. InternalGetInstance(aStubbedInterface);
  49677. end;
  49678. constructor TInterfaceStub.Create(const aGUID: TGUID; out aStubbedInterface);
  49679. begin
  49680. Create(TInterfaceFactory.Get(aGUID),GUIDToRawUTF8(aGUID));
  49681. InternalGetInstance(aStubbedInterface);
  49682. end;
  49683. constructor TInterfaceStub.Create(aInterface: PTypeInfo; out aStubbedInterface);
  49684. begin
  49685. Create(aInterface);
  49686. InternalGetInstance(aStubbedInterface);
  49687. end;
  49688. constructor TInterfaceStub.Create(aInterface: PTypeInfo);
  49689. begin
  49690. Create(TInterfaceFactory.Get(aInterface),ToUTF8(aInterface^.Name));
  49691. end;
  49692. constructor TInterfaceStub.Create(const aGUID: TGUID);
  49693. begin
  49694. Create(TInterfaceFactory.Get(aGUID),ToUTF8(aGUID));
  49695. end;
  49696. procedure TInterfaceStub.IntSetOptions(Options: TInterfaceStubOptions);
  49697. begin
  49698. if Options=fOptions then
  49699. exit;
  49700. fOptions := Options;
  49701. end;
  49702. procedure TInterfaceStub.IntCheckCount(aMethodIndex, aComputed: cardinal;
  49703. aOperator: TSQLQueryOperator; aCount: cardinal);
  49704. const
  49705. OPERATORS: array[qoEqualTo..qoGreaterThanOrEqualTo] of RawUTF8 = (
  49706. '=','<>','<','<=','>','>=');
  49707. function SQLQueryCompare(aOperator: TSQLQueryOperator; A,B: cardinal): boolean;
  49708. begin
  49709. case aOperator of
  49710. qoEqualTo: result := A=B;
  49711. qoNotEqualTo: result := A<>B;
  49712. qoLessThan: result := A<B;
  49713. qoLessThanOrEqualTo: result := A<=B;
  49714. qoGreaterThan: result := A>B;
  49715. qoGreaterThanOrEqualTo: result := A>=B;
  49716. else raise EInterfaceStub.CreateUTF8('%.IntCheckCount(): Unexpected % operator',
  49717. [self,Ord(aOperator)]);
  49718. end;
  49719. end;
  49720. begin
  49721. InternalCheck(SQLQueryCompare(aOperator,aComputed,aCount),True,
  49722. 'ExpectsCount(''%'',%,%) failed: count=%',[fInterface.Methods[aMethodIndex].URI,
  49723. ToText(aOperator)^,aCount,aComputed]);
  49724. end;
  49725. procedure TInterfaceStub.InstanceDestroyed(aClientDrivenID: cardinal);
  49726. var m,r,asmndx: integer;
  49727. num: cardinal;
  49728. begin
  49729. if self<>nil then
  49730. try
  49731. if eCount in fHasExpects then
  49732. for m := 0 to fInterface.MethodsCount-1 do
  49733. with fRules[m] do
  49734. for r := 0 to high(Rules) do
  49735. with Rules[r] do
  49736. if ExpectedPassCountOperator<>qoNone then begin
  49737. if Params='' then
  49738. num := MethodPassCount else
  49739. num := RulePassCount;
  49740. IntCheckCount(m,num,ExpectedPassCountOperator,ExpectedPassCount);
  49741. end;
  49742. if fInterfaceExpectedTraceHash<>0 then
  49743. InternalCheck(LogHash=fInterfaceExpectedTraceHash,True,
  49744. 'ExpectsTrace(%) returned %',[fInterfaceExpectedTraceHash,LogHash]);
  49745. if eTrace in fHasExpects then
  49746. for m := 0 to fInterface.MethodsCount-1 do
  49747. with fRules[m] do begin
  49748. asmndx := m+RESERVED_VTABLE_SLOTS;
  49749. for r := 0 to high(Rules) do
  49750. with Rules[r] do
  49751. if ExpectedTraceHash<>0 then
  49752. InternalCheck(ExpectedTraceHash=Hash32(IntGetLogAsText(
  49753. asmndx,Params,[wName,wParams,wResults],',')),True,
  49754. 'ExpectsTrace(''%'') failed',[fInterface.Methods[m].URI]);
  49755. end;
  49756. finally
  49757. if not (imoFakeInstanceWontReleaseTInterfaceStub in Options) then
  49758. Free; // creature will release its creator
  49759. end;
  49760. end;
  49761. function TInterfaceStub.SetOptions(Options: TInterfaceStubOptions): TInterfaceStub;
  49762. begin
  49763. IntSetOptions(Options);
  49764. result := self;
  49765. end;
  49766. function TInterfaceStub.Executes(const aMethodName, aParams: RawUTF8;
  49767. aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8): TInterfaceStub;
  49768. begin
  49769. fRules[fInterface.CheckMethodIndex(aMethodName)].
  49770. AddRule(self,isExecutesJSON,aParams,aEventParams,TNotifyEvent(aEvent));
  49771. result := self;
  49772. end;
  49773. function TInterfaceStub.Executes(const aMethodName: RawUTF8;
  49774. aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8): TInterfaceStub;
  49775. begin
  49776. result := Executes(aMethodName,'',aEvent,aEventParams);
  49777. end;
  49778. function TInterfaceStub.Executes(const aMethodName: RawUTF8;
  49779. const aParams: array of const; aEvent: TOnInterfaceStubExecuteJSON;
  49780. const aEventParams: RawUTF8): TInterfaceStub;
  49781. begin
  49782. result := Executes(aMethodName,JSONEncodeArrayOfConst(aParams,true),
  49783. aEvent,aEventParams);
  49784. end;
  49785. {$ifndef NOVARIANTS}
  49786. function TInterfaceStub.Executes(const aMethodName, aParams: RawUTF8;
  49787. aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8): TInterfaceStub;
  49788. begin
  49789. fRules[fInterface.CheckMethodIndex(aMethodName)].
  49790. AddRule(self,isExecutesVariant,aParams,aEventParams,TNotifyEvent(aEvent));
  49791. result := self;
  49792. end;
  49793. function TInterfaceStub.Executes(const aMethodName: RawUTF8;
  49794. aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8): TInterfaceStub;
  49795. begin
  49796. result := Executes(aMethodName,'',aEvent,aEventParams);
  49797. end;
  49798. function TInterfaceStub.Executes(const aMethodName: RawUTF8;
  49799. const aParams: array of const; aEvent: TOnInterfaceStubExecuteVariant;
  49800. const aEventParams: RawUTF8): TInterfaceStub;
  49801. begin
  49802. result := Executes(aMethodName,JSONEncodeArrayOfConst(aParams,true),
  49803. aEvent,aEventParams);
  49804. end;
  49805. function TInterfaceStub.Executes(aEvent: TOnInterfaceStubExecuteVariant;
  49806. const aEventParams: RawUTF8): TInterfaceStub;
  49807. var i: integer;
  49808. begin
  49809. for i := 0 to fInterface.MethodsCount-1 do
  49810. fRules[i].AddRule(self,isExecutesVariant,'',aEventParams,TNotifyEvent(aEvent));
  49811. result := self;
  49812. end;
  49813. type
  49814. TInterfaceStubExecutesToLog = packed record
  49815. Log: TSynLogClass;
  49816. LogLevel: TSynLogInfo;
  49817. Kind: TServiceMethodParamsDocVariantKind;
  49818. end;
  49819. PInterfaceStubExecutesToLog = ^TInterfaceStubExecutesToLog;
  49820. procedure TInterfaceStub.OnExecuteToLog(Ctxt: TOnInterfaceStubExecuteParamsVariant);
  49821. begin
  49822. if length(Ctxt.EventParams)=sizeof(TInterfaceStubExecutesToLog) then
  49823. with PInterfaceStubExecutesToLog(Ctxt.EventParams)^ do
  49824. Log.Add.Log(LogLevel,'% %',[Ctxt.Method^.InterfaceDotMethodName,
  49825. Ctxt.InputAsDocVariant(Kind,JSON_OPTIONS_FAST_EXTENDED)]);
  49826. end;
  49827. function TInterfaceStub.Executes(aLog: TSynLogClass; aLogLevel: TSynLogInfo;
  49828. aKind: TServiceMethodParamsDocVariantKind): TInterfaceStub;
  49829. var tmp: RawUTF8;
  49830. begin
  49831. SetLength(tmp,SizeOf(TInterfaceStubExecutesToLog));
  49832. with PInterfaceStubExecutesToLog(tmp)^ do begin
  49833. Log := aLog;
  49834. LogLevel := aLogLevel;
  49835. Kind := aKind;
  49836. end;
  49837. Executes(OnExecuteToLog,tmp);
  49838. result := self;
  49839. end;
  49840. {$endif NOVARIANTS}
  49841. function TInterfaceStub.ExpectsCount(const aMethodName: RawUTF8; aOperator: TSQLQueryOperator;
  49842. aValue: cardinal): TInterfaceStub;
  49843. begin
  49844. result := ExpectsCount(aMethodName,'',aOperator,aValue);
  49845. end;
  49846. function TInterfaceStub.ExpectsCount(const aMethodName, aParams: RawUTF8; aOperator: TSQLQueryOperator;
  49847. aValue: cardinal): TInterfaceStub;
  49848. var ndx: integer;
  49849. begin
  49850. ndx := fInterface.CheckMethodIndex(aMethodName);
  49851. if aOperator in [qoEqualTo..qoGreaterThanOrEqualTo] then
  49852. with fRules[ndx] do
  49853. AddRule(self,isUndefined,aParams,'',nil,nil,aOperator,aValue) else
  49854. raise EInterfaceStub.Create(self,fInterface.fMethods[ndx],
  49855. 'ExpectsCount(aOperator=%)',[ord(aOperator)]);
  49856. include(fHasExpects,eCount);
  49857. result := self;
  49858. end;
  49859. function TInterfaceStub.ExpectsCount(const aMethodName: RawUTF8;
  49860. const aParams: array of const; aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub;
  49861. begin
  49862. result := ExpectsCount(aMethodName,JSONEncodeArrayOfConst(aParams,true),aOperator,aValue);
  49863. end;
  49864. function TInterfaceStub.ExpectsTrace(aValue: cardinal): TInterfaceStub;
  49865. begin
  49866. include(fOptions,imoLogMethodCallsAndResults);
  49867. fInterfaceExpectedTraceHash := aValue;
  49868. result := self;
  49869. end;
  49870. function TInterfaceStub.ExpectsTrace(const aMethodName: RawUTF8; aValue: cardinal): TInterfaceStub;
  49871. begin
  49872. result := ExpectsTrace(aMethodName,'',aValue);
  49873. end;
  49874. function TInterfaceStub.ExpectsTrace(const aMethodName, aParams: RawUTF8;
  49875. aValue: cardinal): TInterfaceStub;
  49876. begin
  49877. fRules[fInterface.CheckMethodIndex(aMethodName)].
  49878. AddRule(self,isUndefined,aParams,'',nil,nil,qoContains,aValue);
  49879. include(fOptions,imoLogMethodCallsAndResults);
  49880. include(fHasExpects,eTrace);
  49881. result := self;
  49882. end;
  49883. function TInterfaceStub.ExpectsTrace(const aMethodName: RawUTF8;
  49884. const aParams: array of const; aValue: cardinal): TInterfaceStub;
  49885. begin
  49886. result := ExpectsTrace(aMethodName,JSONEncodeArrayOfConst(aParams,true),aValue);
  49887. end;
  49888. function TInterfaceStub.ExpectsTrace(const aValue: RawUTF8): TInterfaceStub;
  49889. begin
  49890. result := ExpectsTrace(Hash32(aValue));
  49891. end;
  49892. function TInterfaceStub.ExpectsTrace(const aMethodName, aValue: RawUTF8): TInterfaceStub;
  49893. begin
  49894. result := ExpectsTrace(aMethodName,Hash32(aValue));
  49895. end;
  49896. function TInterfaceStub.ExpectsTrace(const aMethodName, aParams, aValue: RawUTF8): TInterfaceStub;
  49897. begin
  49898. result := ExpectsTrace(aMethodName,aParams,Hash32(aValue));
  49899. end;
  49900. function TInterfaceStub.ExpectsTrace(const aMethodName: RawUTF8; const aParams: array of const;
  49901. const aValue: RawUTF8): TInterfaceStub;
  49902. begin
  49903. result := ExpectsTrace(aMethodName,aParams,Hash32(aValue));
  49904. end;
  49905. function TInterfaceStub.Fails(const aMethodName, aErrorMsg: RawUTF8): TInterfaceStub;
  49906. begin
  49907. result := Fails(aMethodName,'',aErrorMsg);
  49908. end;
  49909. function TInterfaceStub.Fails(const aMethodName, aParams, aErrorMsg: RawUTF8): TInterfaceStub;
  49910. begin
  49911. fRules[fInterface.CheckMethodIndex(aMethodName)].
  49912. AddRule(self,isFails,aParams,aErrorMsg);
  49913. result := self;
  49914. end;
  49915. function TInterfaceStub.Fails(const aMethodName: RawUTF8; const aParams: array of const;
  49916. const aErrorMsg: RawUTF8): TInterfaceStub;
  49917. begin
  49918. result := Fails(aMethodName,JSONEncodeArrayOfConst(aParams,true),aErrorMsg);
  49919. end;
  49920. function TInterfaceStub.Raises(const aMethodName, aParams: RawUTF8;
  49921. aException: ExceptClass; const aMessage: string): TInterfaceStub;
  49922. begin
  49923. fRules[fInterface.CheckMethodIndex(aMethodName)].
  49924. AddRule(self,isRaises,aParams,StringToUTF8(aMessage),nil,aException);
  49925. result := self;
  49926. end;
  49927. function TInterfaceStub.Raises(const aMethodName: RawUTF8;
  49928. const aParams: array of const; aException: ExceptClass;
  49929. const aMessage: string): TInterfaceStub;
  49930. begin
  49931. result := Raises(aMethodName,JSONEncodeArrayOfConst(aParams,true),
  49932. aException,aMessage);
  49933. end;
  49934. function TInterfaceStub.Raises(const aMethodName: RawUTF8;
  49935. aException: ExceptClass; const aMessage: string): TInterfaceStub;
  49936. begin
  49937. result := Raises(aMethodName,'',aException,aMessage);
  49938. end;
  49939. function TInterfaceStub.Returns(const aMethodName, aParams,
  49940. aExpectedResults: RawUTF8): TInterfaceStub;
  49941. begin
  49942. fRules[fInterface.CheckMethodIndex(aMethodName)].
  49943. AddRule(self,isReturns,aParams,aExpectedResults);
  49944. result := self;
  49945. end;
  49946. function TInterfaceStub.Returns(const aMethodName: RawUTF8; const aParams,
  49947. aExpectedResults: array of const): TInterfaceStub;
  49948. begin
  49949. result := Returns(aMethodName,JSONEncodeArrayOfConst(aParams,true),
  49950. JSONEncodeArrayOfConst(aExpectedResults,true));
  49951. end;
  49952. function TInterfaceStub.Returns(const aMethodName,
  49953. aExpectedResults: RawUTF8): TInterfaceStub;
  49954. begin
  49955. result := Returns(aMethodName,'',aExpectedResults);
  49956. end;
  49957. function TInterfaceStub.Returns(const aMethodName: RawUTF8;
  49958. const aExpectedResults: array of const): TInterfaceStub;
  49959. begin
  49960. result := Returns(aMethodName,'',JSONEncodeArrayOfConst(aExpectedResults,true));
  49961. end;
  49962. function TInterfaceStub.Invoke(const aMethod: TServiceMethod;
  49963. const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8;
  49964. aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean;
  49965. var ndx: cardinal;
  49966. rule: integer;
  49967. ExecutesCtxtJSON: TOnInterfaceStubExecuteParamsJSON;
  49968. ExecutesCtxtVariant: TOnInterfaceStubExecuteParamsVariant;
  49969. Log: TInterfaceStubLog;
  49970. begin
  49971. ndx := aMethod.ExecutionMethodIndex-RESERVED_VTABLE_SLOTS;
  49972. if ndx>=fInterface.MethodsCount then
  49973. result := false else
  49974. with fRules[ndx] do begin
  49975. inc(MethodPassCount);
  49976. rule := FindStrongRuleIndex(aParams);
  49977. if rule<0 then begin
  49978. rule := FindRuleIndex(aParams);
  49979. if (rule>=0) and (DefaultRule>=0) then
  49980. inc(Rules[rule].RulePassCount);
  49981. rule := DefaultRule;
  49982. end;
  49983. if rule<0 then
  49984. if imoRaiseExceptionIfNoRuleDefined in Options then
  49985. raise EInterfaceStub.Create(self,aMethod,'No rule defined') else begin
  49986. rule := FindRuleIndex(aParams);
  49987. if rule>=0 then
  49988. inc(Rules[rule].RulePassCount);
  49989. if imoReturnErrorIfNoRuleDefined in Options then begin
  49990. result := false;
  49991. Log.CustomResults := FormatUTF8('No stubbing rule defined for %.%',
  49992. [fInterface.fInterfaceName,aMethod.URI]);
  49993. end else
  49994. result := true;
  49995. end else
  49996. with Rules[rule] do begin
  49997. inc(RulePassCount);
  49998. case Kind of
  49999. isExecutesJSON: begin
  50000. ExecutesCtxtJSON := TOnInterfaceStubExecuteParamsJSON.Create(
  50001. self,@aMethod,aParams,Values);
  50002. try
  50003. TOnInterfaceStubExecuteJSON(Execute)(ExecutesCtxtJSON);
  50004. result := not ExecutesCtxtJSON.Failed;
  50005. Log.CustomResults := ExecutesCtxtJSON.Result;
  50006. finally
  50007. ExecutesCtxtJSON.Free;
  50008. end;
  50009. end;
  50010. {$ifndef NOVARIANTS}
  50011. isExecutesVariant: begin
  50012. ExecutesCtxtVariant := TOnInterfaceStubExecuteParamsVariant.Create(
  50013. self,@aMethod,aParams,Values);
  50014. try
  50015. TOnInterfaceStubExecuteVariant(Execute)(ExecutesCtxtVariant);
  50016. result := not ExecutesCtxtVariant.Failed;
  50017. if result then begin
  50018. ExecutesCtxtVariant.SetResultFromOutput;
  50019. Log.CustomResults := ExecutesCtxtVariant.Result;
  50020. end;
  50021. finally
  50022. ExecutesCtxtVariant.Free;
  50023. end;
  50024. end;
  50025. {$endif}
  50026. isRaises:
  50027. raise ExceptionClass.Create(UTF8ToString(Values));
  50028. isReturns: begin
  50029. result := true;
  50030. Log.CustomResults := Values;
  50031. end;
  50032. isFails: begin
  50033. result := InternalCheck(false,false,'%',[Values]);
  50034. if not result then
  50035. Log.CustomResults := Values;
  50036. end;
  50037. else
  50038. result := true; // ignore isUndefined (ExpectsCount only) rules
  50039. end;
  50040. end;
  50041. if result then begin
  50042. if aResult<>nil then // make unique due to JSONDecode()
  50043. if Log.CustomResults='' then
  50044. SetString(aResult^,PAnsiChar(pointer(aMethod.DefaultResult)),
  50045. length(aMethod.DefaultResult)) else
  50046. SetString(aResult^,PAnsiChar(pointer(Log.CustomResults)),
  50047. length(Log.CustomResults));
  50048. end else
  50049. if aErrorMsg<>nil then
  50050. aErrorMsg^ := Log.CustomResults;
  50051. if imoLogMethodCallsAndResults in Options then begin
  50052. Log.TimeStamp64 := GetTickCount64;
  50053. Log.WasError := not result;
  50054. Log.Method := @aMethod;
  50055. Log.Params := aParams;
  50056. fLog.Add(Log);
  50057. end;
  50058. end;
  50059. end;
  50060. function TInterfaceStub.LogAsText(SepChar: AnsiChar): RawUTF8;
  50061. begin
  50062. result := IntGetLogAsText(0,'',[wName,wParams,wResults],SepChar);
  50063. end;
  50064. procedure TInterfaceStub.ClearLog;
  50065. begin
  50066. fLog.Clear;
  50067. end;
  50068. function TInterfaceStub.IntGetLogAsText(asmndx: integer; const aParams: RawUTF8;
  50069. aScope: TInterfaceStubLogLayouts; SepChar: AnsiChar): RawUTF8;
  50070. var i: integer;
  50071. WR: TTextWriter;
  50072. Log: ^TInterfaceStubLog;
  50073. begin
  50074. if fLogCount=0 then
  50075. result := '' else begin
  50076. WR := TTextWriter.CreateOwnedStream;
  50077. try
  50078. Log := Pointer(fLogs);
  50079. if asmndx<RESERVED_VTABLE_SLOTS then
  50080. for i := 1 to fLogCount do begin
  50081. Log^.AddAsText(WR,aScope,SepChar);
  50082. inc(Log);
  50083. end else
  50084. for i := 1 to fLogCount do begin
  50085. if Log^.Method^.ExecutionMethodIndex=asmndx then
  50086. if (aParams='') or (Log^.Params=aParams) then
  50087. Log^.AddAsText(WR,aScope,SepChar);
  50088. inc(Log);
  50089. end;
  50090. WR.CancelLastChar(SepChar);
  50091. WR.SetText(result);
  50092. finally
  50093. WR.Free;
  50094. end;
  50095. end;
  50096. end;
  50097. function TInterfaceStub.GetLogHash: cardinal;
  50098. begin
  50099. result := Hash32(LogAsText);
  50100. end;
  50101. function TInterfaceStub.TryResolve(aInterface: PTypeInfo; out Obj): boolean;
  50102. begin
  50103. if aInterface<>fInterface.fInterfaceTypeInfo then
  50104. result := false else begin
  50105. InternalGetInstance(Obj);
  50106. result := true;
  50107. end;
  50108. end;
  50109. function TInterfaceStub.Implements(aInterface: PTypeInfo): boolean;
  50110. begin
  50111. result := fInterface.fInterfaceTypeInfo=aInterface;
  50112. end;
  50113. { TInterfaceMock }
  50114. constructor TInterfaceMock.Create(aInterface: PTypeInfo;
  50115. out aMockedInterface; aTestCase: TSynTestCase);
  50116. begin
  50117. inherited Create(aInterface,aMockedInterface);
  50118. fTestCase := aTestCase;
  50119. end;
  50120. constructor TInterfaceMock.Create(const aGUID: TGUID; out aMockedInterface;
  50121. aTestCase: TSynTestCase);
  50122. begin
  50123. inherited Create(aGUID,aMockedInterface);
  50124. fTestCase := aTestCase;
  50125. end;
  50126. constructor TInterfaceMock.Create(const aInterfaceName: RawUTF8;
  50127. out aMockedInterface; aTestCase: TSynTestCase);
  50128. begin
  50129. inherited Create(aInterfaceName,aMockedInterface);
  50130. fTestCase := aTestCase;
  50131. end;
  50132. constructor TInterfaceMock.Create(aInterface: PTypeInfo; aTestCase: TSynTestCase);
  50133. begin
  50134. inherited Create(aInterface);
  50135. fTestCase := aTestCase;
  50136. end;
  50137. constructor TInterfaceMock.Create(const aGUID: TGUID; aTestCase: TSynTestCase);
  50138. begin
  50139. inherited Create(aGUID);
  50140. fTestCase := aTestCase;
  50141. end;
  50142. function TInterfaceMock.InternalCheck(aValid,aExpectationFailed: boolean;
  50143. const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean;
  50144. begin
  50145. if fTestCase=nil then
  50146. result := inherited InternalCheck(aValid,aExpectationFailed,aErrorMsgFmt,aErrorMsgArgs) else begin
  50147. if aValid xor (imoMockFailsWillPassTestCase in Options) then
  50148. fTestCase.Check(true) else
  50149. fTestCase.Check(false,UTF8ToString(FormatUTF8(aErrorMsgFmt,aErrorMsgArgs)));
  50150. result := true; // do not raise any exception at this stage for TInterfaceMock
  50151. end;
  50152. end;
  50153. { TInterfaceMockSpy }
  50154. constructor TInterfaceMockSpy.Create(aFactory: TInterfaceFactory;
  50155. const aInterfaceName: RawUTF8);
  50156. begin
  50157. inherited Create(aFactory,aInterfaceName);
  50158. include(fOptions,imoLogMethodCallsAndResults);
  50159. end;
  50160. procedure TInterfaceMockSpy.IntSetOptions(Options: TInterfaceStubOptions);
  50161. begin
  50162. include(Options,imoLogMethodCallsAndResults);
  50163. inherited IntSetOptions(Options);
  50164. end;
  50165. procedure TInterfaceMockSpy.Verify(const aMethodName: RawUTF8;
  50166. const aParams: array of const; aOperator: TSQLQueryOperator;
  50167. aCount: cardinal);
  50168. begin
  50169. Verify(aMethodName,JSONEncodeArrayOfConst(aParams,true),aOperator,aCount);
  50170. end;
  50171. procedure TInterfaceMockSpy.Verify(const aMethodName: RawUTF8;
  50172. const aParams: array of const; const aTrace: RawUTF8);
  50173. begin
  50174. Verify(aMethodName,JSONEncodeArrayOfConst(aParams,true),aTrace);
  50175. end;
  50176. procedure TInterfaceMockSpy.Verify(const aMethodName: RawUTF8;
  50177. aOperator: TSQLQueryOperator; aCount: cardinal);
  50178. var m: integer;
  50179. begin
  50180. m := fInterface.CheckMethodIndex(aMethodName);
  50181. IntCheckCount(m,fRules[m].MethodPassCount,aOperator,aCount);
  50182. end;
  50183. procedure TInterfaceMockSpy.Verify(const aMethodName, aParams: RawUTF8;
  50184. aOperator: TSQLQueryOperator; aCount: cardinal);
  50185. var asmndx, i: integer;
  50186. c: cardinal;
  50187. begin
  50188. asmndx := fInterface.CheckMethodIndex(aMethodName)+RESERVED_VTABLE_SLOTS;
  50189. if aParams='' then
  50190. c := fRules[asmndx-RESERVED_VTABLE_SLOTS].MethodPassCount else begin
  50191. c := 0;
  50192. for i := 0 to fLogCount-1 do
  50193. with fLogs[i] do
  50194. if (Method.ExecutionMethodIndex=asmndx) and (Params=aParams) then
  50195. inc(c);
  50196. end;
  50197. IntCheckCount(asmndx-RESERVED_VTABLE_SLOTS,c,aOperator,aCount);
  50198. end;
  50199. procedure TInterfaceMockSpy.Verify(const aTrace: RawUTF8;
  50200. aScope: TInterfaceMockSpyCheck);
  50201. const
  50202. VERIFY_SCOPE: array[TInterfaceMockSpyCheck] of TInterfaceStubLogLayouts = (
  50203. [wName], [wName, wParams], [wName, wParams, wResults]);
  50204. begin
  50205. InternalCheck(IntGetLogAsText(0,'',VERIFY_SCOPE[aScope],',')=aTrace,true,
  50206. 'Verify(''%'',%) failed',[aTrace,ToText(aScope)^]);
  50207. end;
  50208. procedure TInterfaceMockSpy.Verify(const aMethodName, aParams, aTrace: RawUTF8);
  50209. var m: integer;
  50210. begin
  50211. m := fInterface.CheckMethodIndex(aMethodName);
  50212. InternalCheck(
  50213. IntGetLogAsText(m+RESERVED_VTABLE_SLOTS,aParams,[wResults],',')=aTrace,True,
  50214. 'Verify(''%'',''%'',''%'') failed',[aMethodName,aParams,aTrace]);
  50215. end;
  50216. procedure TInterfaceMockSpy.Verify(const aMethodName, aTrace: RawUTF8;
  50217. aScope: TInterfaceMockSpyCheck);
  50218. const
  50219. VERIFY_SCOPE: array[TInterfaceMockSpyCheck] of TInterfaceStubLogLayouts = (
  50220. [], [wParams], [wParams, wResults]);
  50221. var m: integer;
  50222. begin
  50223. m := fInterface.CheckMethodIndex(aMethodName);
  50224. if aScope=chkName then
  50225. raise EInterfaceStub.Create(self,fInterface.Methods[m],'Invalid scope for Verify()');
  50226. InternalCheck(
  50227. IntGetLogAsText(m+RESERVED_VTABLE_SLOTS,'',VERIFY_SCOPE[aScope],',')=aTrace,True,
  50228. 'Verify(''%'',''%'',%) failed',[aMethodName,aTrace,ToText(aScope)^]);
  50229. end;
  50230. { TInterfaceResolverForSingleInterface }
  50231. constructor TInterfaceResolverForSingleInterface.Create(
  50232. aInterface: PTypeInfo; aImplementation: TInterfacedObjectClass);
  50233. var guid: PGUID;
  50234. begin
  50235. fInterfaceTypeInfo := aInterface;
  50236. guid := aInterface^.InterfaceGUID;
  50237. if guid=nil then
  50238. raise EInterfaceResolverException.CreateUTF8('%.Create expects an Interface',[self]);
  50239. fImplementationEntry := aImplementation.GetInterfaceEntry(guid^);
  50240. if fImplementationEntry=nil then
  50241. raise EInterfaceResolverException.CreateUTF8('%.Create: % does not implement %',
  50242. [self,aImplementation,fInterfaceTypeInfo^.Name]);
  50243. aInterface^.InterfaceAncestors(fInterfaceAncestors,aImplementation,
  50244. fInterfaceAncestorsImplementationEntry);
  50245. fImplementation.Init(aImplementation);
  50246. end;
  50247. constructor TInterfaceResolverForSingleInterface.Create(const aInterface: TGUID;
  50248. aImplementation: TInterfacedObjectClass);
  50249. begin
  50250. Create(TInterfaceFactory.GUID2TypeInfo(aInterface),aImplementation);
  50251. end;
  50252. function TInterfaceResolverForSingleInterface.CreateInstance: TInterfacedObject;
  50253. begin
  50254. result := TInterfacedObject(fImplementation.CreateNew);
  50255. end;
  50256. function TInterfaceResolverForSingleInterface.GetImplementationName: string;
  50257. begin
  50258. if self=nil then
  50259. result := '' else
  50260. result := string(fImplementation.ItemClass.ClassName);
  50261. end;
  50262. function TInterfaceResolverForSingleInterface.GetOneInstance(out Obj): boolean;
  50263. begin
  50264. if self=nil then
  50265. result := false else
  50266. // here we now that CreateInstance will implement the interface
  50267. result := GetInterfaceFromEntry(CreateInstance,fImplementationEntry,Obj);
  50268. end;
  50269. function TInterfaceResolverForSingleInterface.TryResolve(
  50270. aInterface: PTypeInfo; out Obj): boolean;
  50271. var i: integer;
  50272. begin
  50273. if fInterfaceTypeInfo=aInterface then
  50274. result := GetInterfaceFromEntry(
  50275. CreateInstance,fImplementationEntry,Obj) else begin
  50276. // if not found exact interface, try any parent/ancestor interface
  50277. for i := 0 to length(fInterfaceAncestors)-1 do
  50278. if fInterfaceAncestors[i]=aInterface then begin
  50279. // here we know that CreateInstance will implement fInterfaceAncestors[]
  50280. result := GetInterfaceFromEntry(
  50281. CreateInstance,fInterfaceAncestorsImplementationEntry[i],Obj);
  50282. exit;
  50283. end;
  50284. result := false;
  50285. end;
  50286. end;
  50287. function TInterfaceResolverForSingleInterface.Implements(aInterface: PTypeInfo): boolean;
  50288. var i: integer;
  50289. begin
  50290. if fInterfaceTypeInfo=aInterface then
  50291. result := true else begin
  50292. // if not found exact interface, try any parent/ancestor interface
  50293. for i := 0 to length(fInterfaceAncestors)-1 do
  50294. if fInterfaceAncestors[i]=aInterface then begin
  50295. result := true;
  50296. exit;
  50297. end;
  50298. result := false;
  50299. end;
  50300. end;
  50301. { TInterfaceResolverInjected }
  50302. var
  50303. GlobalInterfaceResolutionLock: TRTLCriticalSection;
  50304. GlobalInterfaceResolution: array of record
  50305. TypeInfo: PTypeInfo;
  50306. ImplementationClass: TInterfacedObjectWithCustomCreateClass;
  50307. ImplementationInstance: TInterfacedObject;
  50308. InterfaceEntry: PInterfaceEntry;
  50309. end;
  50310. class function TInterfaceResolverInjected.RegisterGlobalCheck(aInterface: PTypeInfo;
  50311. aImplementationClass: TClass): PInterfaceEntry;
  50312. var i: integer;
  50313. begin
  50314. if (aInterface=nil) or (aImplementationClass=nil) then
  50315. raise EInterfaceResolverException.CreateUTF8('%.RegisterGlobal(nil)',[self]);
  50316. if aInterface^.Kind<>tkInterface then
  50317. raise EInterfaceResolverException.CreateUTF8(
  50318. '%.RegisterGlobal(%): % is not an interface',
  50319. [self,aInterface^.Name,aInterface^.Name]);
  50320. //alfchange
  50321. //result := aImplementationClass.GetInterfaceEntry(
  50322. // PInterfaceTypeData(aInterface^.ClassType)^.IntfGuid);
  50323. result := aImplementationClass.GetInterfaceEntry(aInterface^.InterfaceGUID^);
  50324. if result=nil then
  50325. raise EInterfaceResolverException.CreateUTF8(
  50326. '%.RegisterGlobal(): % does not implement %',
  50327. [self,aImplementationClass,aInterface^.Name]);
  50328. EnterCriticalSection(GlobalInterfaceResolutionLock);
  50329. for i := 0 to length(GlobalInterfaceResolution)-1 do
  50330. if GlobalInterfaceResolution[i].TypeInfo=aInterface then begin
  50331. LeaveCriticalSection(GlobalInterfaceResolutionLock); // release fSafe.Lock now
  50332. raise EInterfaceResolverException.CreateUTF8(
  50333. '%.RegisterGlobal(%): % already registered',
  50334. [self,aImplementationClass,aInterface^.Name]);
  50335. end;
  50336. end; // caller should explicitly call finally LeaveCriticalSection(...) end;
  50337. class procedure TInterfaceResolverInjected.RegisterGlobal(
  50338. aInterface: PTypeInfo; aImplementationClass: TInterfacedObjectWithCustomCreateClass);
  50339. var aInterfaceEntry: PInterfaceEntry;
  50340. n: integer;
  50341. begin
  50342. aInterfaceEntry := RegisterGlobalCheck(aInterface,aImplementationClass);
  50343. try // here we are protected within a EnterCriticalSection() call
  50344. n := length(GlobalInterfaceResolution);
  50345. SetLength(GlobalInterfaceResolution,n+1);
  50346. with GlobalInterfaceResolution[n] do begin
  50347. TypeInfo := aInterface;
  50348. ImplementationClass := aImplementationClass;
  50349. InterfaceEntry := aInterfaceEntry;
  50350. end;
  50351. finally
  50352. LeaveCriticalSection(GlobalInterfaceResolutionLock);
  50353. end;
  50354. end;
  50355. class procedure TInterfaceResolverInjected.RegisterGlobal(
  50356. aInterface: PTypeInfo; aImplementation: TInterfacedObject);
  50357. var aInterfaceEntry: PInterfaceEntry;
  50358. n: integer;
  50359. begin
  50360. aInterfaceEntry := RegisterGlobalCheck(aInterface,aImplementation.ClassType);
  50361. try // here we are protected within a EnterCriticalSection() call
  50362. n := length(GlobalInterfaceResolution);
  50363. SetLength(GlobalInterfaceResolution,n+1);
  50364. with GlobalInterfaceResolution[n] do begin
  50365. TypeInfo := aInterface;
  50366. IInterface(aImplementation)._AddRef;
  50367. ImplementationInstance := aImplementation;
  50368. InterfaceEntry := aInterfaceEntry;
  50369. end;
  50370. finally
  50371. LeaveCriticalSection(GlobalInterfaceResolutionLock);
  50372. end;
  50373. end;
  50374. class procedure TInterfaceResolverInjected.RegisterGlobalDelete(aInterface: PTypeInfo);
  50375. var i: integer;
  50376. begin
  50377. if (aInterface=nil) or (aInterface^.Kind<>tkInterface) then
  50378. raise EInterfaceResolverException.CreateUTF8('%.RegisterGlobalDelete(?)',[self]);
  50379. EnterCriticalSection(GlobalInterfaceResolutionLock);
  50380. try
  50381. for i := 0 to length(GlobalInterfaceResolution)-1 do
  50382. with GlobalInterfaceResolution[i] do
  50383. if TypeInfo=aInterface then
  50384. if ImplementationInstance=nil then
  50385. raise EInterfaceResolverException.CreateUTF8(
  50386. '%.RegisterGlobalDelete(%) does not match an instance, but a class',
  50387. [self,aInterface^.Name]) else begin
  50388. IInterface(ImplementationInstance)._Release;
  50389. exit;
  50390. end;
  50391. finally
  50392. LeaveCriticalSection(GlobalInterfaceResolutionLock);
  50393. end;
  50394. end;
  50395. procedure FinalizeGlobalInterfaceResolution;
  50396. var i: Integer;
  50397. begin
  50398. for i := length(GlobalInterfaceResolution)-1 downto 0 do
  50399. with GlobalInterfaceResolution[i] do
  50400. if ImplementationInstance<>nil then
  50401. try
  50402. ImplementationInstance.Free;
  50403. except
  50404. end;
  50405. DeleteCriticalSection(GlobalInterfaceResolutionLock);
  50406. end;
  50407. function TInterfaceResolverInjected.TryResolve(aInterface: PTypeInfo; out Obj): boolean;
  50408. var i: integer;
  50409. begin
  50410. if aInterface<>nil then begin
  50411. result := true;
  50412. if self<>nil then begin // first check local DI/IoC
  50413. if fResolvers<>nil then
  50414. for i := 0 to length(fResolvers)-1 do
  50415. if fResolvers[i].TryResolve(aInterface,Obj) then
  50416. exit;
  50417. if fDependencies<>nil then
  50418. for i := 0 to Length(fDependencies)-1 do
  50419. if fDependencies[i].GetInterface(aInterface^.InterfaceGUID^,Obj) then
  50420. exit;
  50421. end;
  50422. EnterCriticalSection(GlobalInterfaceResolutionLock); // shared DI/IoC
  50423. try
  50424. for i := 0 to length(GlobalInterfaceResolution)-1 do
  50425. with GlobalInterfaceResolution[i] do
  50426. if TypeInfo=aInterface then
  50427. if ImplementationInstance<>nil then begin
  50428. if GetInterfaceFromEntry(ImplementationInstance,InterfaceEntry,Obj) then
  50429. exit;
  50430. end else
  50431. if GetInterfaceFromEntry(ImplementationClass.Create,InterfaceEntry,Obj) then
  50432. exit;
  50433. finally
  50434. LeaveCriticalSection(GlobalInterfaceResolutionLock);
  50435. end;
  50436. end;
  50437. result := false;
  50438. end;
  50439. function TInterfaceResolverInjected.TryResolveInternal(aInterface: PTypeInfo; out Obj): boolean;
  50440. var i: integer;
  50441. begin
  50442. result := true;
  50443. if (self<>nil) and (aInterface<>nil) and (fResolvers<>nil) then
  50444. for i := 0 to length(fResolvers)-1 do
  50445. if fResolvers[i].TryResolve(aInterface,Obj) then
  50446. exit;
  50447. result := false;
  50448. end;
  50449. function TInterfaceResolverInjected.Implements(aInterface: PTypeInfo): boolean;
  50450. var i: integer;
  50451. begin
  50452. result := true;
  50453. if (self<>nil) and (aInterface<>nil) and (fResolvers<>nil) then
  50454. for i := 0 to length(fResolvers)-1 do
  50455. if fResolvers[i].Implements(aInterface) then
  50456. exit;
  50457. result := false;
  50458. end;
  50459. procedure TInterfaceResolverInjected.InjectStub(const aStubsByGUID: array of TGUID);
  50460. var i: integer;
  50461. begin
  50462. for i := 0 to high(aStubsByGUID) do
  50463. InjectResolver([TInterfaceStub.Create(aStubsByGUID[i])]);
  50464. end;
  50465. procedure TInterfaceResolverInjected.InjectResolver(
  50466. const aOtherResolvers: array of TInterfaceResolver;
  50467. OwnOtherResolvers: boolean);
  50468. var i: integer;
  50469. begin
  50470. for i := 0 to high(aOtherResolvers) do
  50471. if aOtherResolvers[i]<>nil then begin
  50472. if aOtherResolvers[i].InheritsFrom(TInterfaceStub) then begin
  50473. include(TInterfaceStub(aOtherResolvers[i]).fOptions,
  50474. imoFakeInstanceWontReleaseTInterfaceStub);
  50475. ObjArrayAdd(fResolversToBeReleased,aOtherResolvers[i]);
  50476. end else
  50477. if OwnOtherResolvers then
  50478. ObjArrayAdd(fResolversToBeReleased,aOtherResolvers[i]);
  50479. ObjArrayAddOnce(fResolvers,aOtherResolvers[i]);
  50480. end;
  50481. end;
  50482. procedure TInterfaceResolverInjected.InjectInstance(
  50483. const aDependencies: array of TInterfacedObject);
  50484. var i: integer;
  50485. begin
  50486. for i := 0 to high(aDependencies) do
  50487. if aDependencies[i]<>nil then begin
  50488. IInterface(aDependencies[i])._AddRef; // Destroy will do _Release
  50489. ObjArrayAdd(fDependencies,aDependencies[i]);
  50490. end;
  50491. end;
  50492. destructor TInterfaceResolverInjected.Destroy;
  50493. var i: integer;
  50494. begin
  50495. try
  50496. ObjArrayClear(fResolversToBeReleased);
  50497. for i := 0 to length(fDependencies)-1 do
  50498. IInterface(fDependencies[i])._Release;
  50499. finally
  50500. inherited Destroy;
  50501. end;
  50502. end;
  50503. function TInterfaceResolverInjected.Resolve(aInterface: PTypeInfo; out Obj): boolean;
  50504. begin
  50505. if self=nil then
  50506. result := false else
  50507. result := TryResolve(aInterface,Obj);
  50508. end;
  50509. function TInterfaceResolverInjected.Resolve(const aGUID: TGUID; out Obj): boolean;
  50510. var known: TInterfaceFactory;
  50511. begin
  50512. if self=nil then
  50513. result := false else begin
  50514. known := TInterfaceFactory.Get(aGUID);
  50515. if known<>nil then
  50516. result := Resolve(known.fInterfaceTypeInfo,Obj) else
  50517. result := false;
  50518. end;
  50519. end;
  50520. procedure TInterfaceResolverInjected.ResolveByPair(
  50521. const aInterfaceObjPairs: array of pointer; aRaiseExceptionIfNotFound: boolean);
  50522. var n,i: integer;
  50523. begin
  50524. n := length(aInterfaceObjPairs);
  50525. if (n=0) or (n and 1=1) then
  50526. raise EServiceException.CreateUTF8('%.Resolve([odd])',[self]);
  50527. for i := 0 to (n shr 1)-1 do
  50528. if not Resolve(aInterfaceObjPairs[i*2],aInterfaceObjPairs[i*2+1]^) then
  50529. if aRaiseExceptionIfNotFound then
  50530. raise EServiceException.CreateUTF8('%.ResolveByPair(%) unsatisfied',
  50531. [self,PTypeInfo(aInterfaceObjPairs[i*2])^.Name]);
  50532. end;
  50533. procedure TInterfaceResolverInjected.Resolve(const aInterfaces: array of TGUID;
  50534. const aObjs: array of pointer; aRaiseExceptionIfNotFound: boolean);
  50535. var n,i: integer;
  50536. info: PTypeInfo;
  50537. begin
  50538. n := length(aInterfaces);
  50539. if (n=0) or (n<>length(aObjs)) then
  50540. raise EServiceException.CreateUTF8('%.Resolve([?,?])',[self]);
  50541. for i := 0 to n-1 do
  50542. if PPointer(aObjs[i])^=nil then begin
  50543. info := TInterfaceFactory.GUID2TypeInfo(aInterfaces[i]);
  50544. if not Resolve(info,aObjs[i]^) then
  50545. if aRaiseExceptionIfNotFound then
  50546. raise EServiceException.CreateUTF8('%.Resolve(%) unsatisfied',[self,info^.Name]);
  50547. end;
  50548. end;
  50549. { TInjectableObject }
  50550. function TInjectableObject.TryResolve(aInterface: PTypeInfo; out Obj): boolean;
  50551. begin
  50552. if (self<>nil) and (aInterface<>nil) and (fResolver<>nil) then
  50553. result := fResolver.TryResolve(aInterface,Obj) else
  50554. result := false;
  50555. end;
  50556. procedure TInjectableObject.Resolve(aInterface: PTypeInfo; out Obj);
  50557. begin
  50558. if not TryResolve(aInterface,Obj) then
  50559. raise EServiceException.CreateUTF8('%.Resolve(%) unsatisfied',[self,aInterface^.Name]);
  50560. end;
  50561. procedure TInjectableObject.Resolve(const aGUID: TGUID; out Obj);
  50562. var info: PTypeInfo;
  50563. begin
  50564. info := TInterfaceFactory.GUID2TypeInfo(aGUID);
  50565. if not TryResolve(info,Obj) then
  50566. raise EServiceException.CreateUTF8(
  50567. '%.Resolve(%): Interface not registered',[self,info^.Name]);
  50568. end;
  50569. procedure TInjectableObject.ResolveByPair(const aInterfaceObjPairs: array of pointer);
  50570. begin
  50571. if fResolver.InheritsFrom(TInterfaceResolverInjected) then
  50572. TInterfaceResolverInjected(fResolver).ResolveByPair(aInterfaceObjPairs) else
  50573. if high(aInterfaceObjPairs)=1 then
  50574. Resolve(aInterfaceObjPairs[0],aInterfaceObjPairs[1]^) else
  50575. raise EServiceException.CreateUTF8('%.ResolveByPair(?)',[self]);
  50576. end;
  50577. procedure TInjectableObject.Resolve(const aInterfaces: array of TGUID;
  50578. const aObjs: array of pointer);
  50579. begin
  50580. if fResolver.InheritsFrom(TInterfaceResolverInjected) then
  50581. TInterfaceResolverInjected(fResolver).Resolve(aInterfaces,aObjs) else
  50582. if (high(aInterfaces)=0) and (high(aObjs)=0) then
  50583. Resolve(aInterfaces[0],aObjs[0]^) else
  50584. raise EServiceException.CreateUTF8('%.Resolve(?,?)',[self]);
  50585. end;
  50586. procedure TInjectableObject.AutoResolve(aRaiseEServiceExceptionIfNotFound: boolean);
  50587. var i: integer;
  50588. CT: TClass;
  50589. P: PPropInfo;
  50590. addr: pointer;
  50591. begin
  50592. if (self=nil) or (fResolver=nil) then
  50593. raise EServiceException.CreateUTF8('%.AutoResolve with no prior registration',[self]);
  50594. CT := ClassType;
  50595. if CT<>TInjectableObject then
  50596. repeat
  50597. for i := 1 to InternalClassPropInfo(CT,P) do begin
  50598. if P^.PropType^.Kind=tkInterface then
  50599. if P^.GetterIsField then begin
  50600. addr := P^.GetterAddr(self);
  50601. if not TryResolve(P^.TypeInfo,addr^) then
  50602. if aRaiseEServiceExceptionIfNotFound then
  50603. raise EServiceException.CreateUTF8(
  50604. '%.AutoResolve: impossible to resolve published property %: %',
  50605. [self,P^.Name,P^.PropType^.Name]);
  50606. end else
  50607. raise EServiceException.CreateUTF8(
  50608. '%.AutoResolve: published property %: % should directly read the field',
  50609. [self,P^.Name,P^.PropType^.Name]);
  50610. P := P^.Next;
  50611. end;
  50612. CT := CT.ClassParent;
  50613. until CT=TInjectableObject;
  50614. end;
  50615. constructor TInjectableObject.CreateInjected(const aStubsByGUID: array of TGUID;
  50616. const aOtherResolvers: array of TInterfaceResolver;
  50617. const aDependencies: array of TInterfacedObject;
  50618. aRaiseEServiceExceptionIfNotFound: boolean);
  50619. begin
  50620. fResolver := TInterfaceResolverInjected.Create;
  50621. fResolverOwned := true;
  50622. TInterfaceResolverInjected(fResolver).InjectStub(aStubsByGUID);
  50623. TInterfaceResolverInjected(fResolver).InjectResolver(aOtherResolvers);
  50624. TInterfaceResolverInjected(fResolver).InjectInstance(aDependencies);
  50625. Create;
  50626. AutoResolve(aRaiseEServiceExceptionIfNotFound);
  50627. end;
  50628. constructor TInjectableObject.CreateWithResolver(aResolver: TInterfaceResolver;
  50629. aRaiseEServiceExceptionIfNotFound: boolean);
  50630. begin
  50631. if fResolver<>nil then
  50632. exit; // inject once!
  50633. if aResolver=nil then
  50634. raise EServiceException.CreateUTF8('%.CreateWithResolver(nil)',[self]);
  50635. fResolver := aResolver; // may be needed by overriden Create
  50636. Create;
  50637. AutoResolve(aRaiseEServiceExceptionIfNotFound);
  50638. end;
  50639. destructor TInjectableObject.Destroy;
  50640. begin
  50641. inherited Destroy;
  50642. CleanupInstance; // ensure creatures are released before their creator
  50643. if fResolverOwned then
  50644. FreeAndNil(fResolver); // let the creator move away
  50645. end;
  50646. { TServiceFactory }
  50647. function TServiceFactory.GetInterfaceTypeInfo: PTypeInfo;
  50648. begin
  50649. if (Self<>nil) and (fInterface<>nil) then
  50650. result := fInterface.fInterfaceTypeInfo else
  50651. result := nil;
  50652. end;
  50653. function TServiceFactory.GetInterfaceIID: TGUID;
  50654. begin
  50655. result := fInterface.fInterfaceIID;
  50656. end;
  50657. constructor TServiceFactory.Create(aRest: TSQLRest;
  50658. aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation;
  50659. const aContractExpected: RawUTF8);
  50660. var m,j: integer;
  50661. begin
  50662. // check supplied interface
  50663. if (aRest=nil) or (aInterface=nil) then
  50664. raise EServiceException.CreateUTF8('Invalid %.Create(%,%)',[self,aRest,aInterface]);
  50665. inherited Create;
  50666. fInterface := TInterfaceFactory.Get(aInterface);
  50667. fRest := aRest;
  50668. fInstanceCreation := aInstanceCreation;
  50669. fInterfaceMangledURI := BinToBase64URI(@fInterface.fInterfaceIID,sizeof(TGUID));
  50670. fInterfaceURI := ToUTF8(aInterface^.Name);
  50671. if fInterfaceURI[1] in ['I','i'] then
  50672. delete(fInterfaceURI,1,1);
  50673. if fRest.Model.GetTableIndex(fInterfaceURI)>=0 then
  50674. raise EServiceException.CreateUTF8('%.Create: "%" interface name '+
  50675. 'is already used by a SQL table name',[self,fInterfaceURI]);
  50676. for m := 0 to fInterface.fMethodsCount-1 do
  50677. with fInterface.fMethods[m] do begin
  50678. if ArgsResultIndex>=0 then
  50679. with Args[ArgsResultIndex] do
  50680. case ValueType of
  50681. smvNone, smvObject, smvInterface:
  50682. raise EServiceException.CreateUTF8('%.Create: %.% unexpected result type %',
  50683. [self,fInterface.fInterfaceName,URI,ArgTypeName^]);
  50684. smvRecord:
  50685. if ArgTypeInfo=System.TypeInfo(TServiceCustomAnswer) then
  50686. if InstanceCreation=sicClientDriven then
  50687. raise EServiceException.CreateUTF8('%.Create: %.% '+
  50688. 'sicClientDriven mode not allowed with TServiceCustomAnswer result',
  50689. [self,fInterface.fInterfaceName,URI]) else begin
  50690. for j := ArgsOutFirst to ArgsOutLast do
  50691. if Args[j].ValueDirection in [smdVar,smdOut] then
  50692. raise EServiceException.CreateUTF8('%.Create: %.% '+
  50693. 'var/out parameter "%" not allowed with TServiceCustomAnswer result',
  50694. [self,fInterface.fInterfaceName,URI,Args[j].ParamName^]);
  50695. ArgsResultIsServiceCustomAnswer := true;
  50696. end;
  50697. end;
  50698. end;
  50699. SetLength(fExecution,fInterface.fMethodsCount);
  50700. // compute interface signature (aka "contract"), serialized as a JSON object
  50701. fContract := FormatUTF8('{"contract":"%","implementation":"%","methods":%}',
  50702. [InterfaceURI,LowerCase(TrimLeftLowerCaseShort(ToText(InstanceCreation))),
  50703. fInterface.fContract]);
  50704. fContractHash := '"'+CardinalToHex(Hash32(fContract))+
  50705. CardinalToHex(CRC32string(fContract))+'"'; // 2 hashes to avoid collision
  50706. if aContractExpected<>'' then // override default contract
  50707. fContractExpected := aContractExpected else
  50708. fContractExpected := fContractHash; // for security
  50709. end;
  50710. { TServiceContainerServer }
  50711. function TServiceContainerServer.AddImplementation(
  50712. aImplementationClass: TInterfacedClass; const aInterfaces: array of PTypeInfo;
  50713. aInstanceCreation: TServiceInstanceImplementation;
  50714. aSharedImplementation: TInterfacedObject; const aContractExpected: RawUTF8): TServiceFactoryServer;
  50715. var C: TClass;
  50716. T: PInterfaceTable;
  50717. i, j: integer;
  50718. UID: array of ^TGUID;
  50719. F: TServiceFactoryServer;
  50720. begin
  50721. result := nil;
  50722. // check input parameters
  50723. if (self=nil) or (aImplementationClass=nil) or (high(aInterfaces)<0) then
  50724. exit;
  50725. if aSharedImplementation<>nil then
  50726. if (aSharedImplementation.ClassType<>aImplementationClass) or
  50727. (aInstanceCreation<>sicShared) then
  50728. raise EServiceException.CreateUTF8('%.AddImplementation: invalid % class',
  50729. [self,aSharedImplementation]);
  50730. CheckInterface(aInterfaces);
  50731. SetLength(UID,length(aInterfaces));
  50732. for j := 0 to high(aInterfaces) do
  50733. UID[j] := pointer(aInterfaces[j]^.InterfaceGUID);
  50734. //UID[j] := @PInterfaceTypeData(aInterfaces[j]^.ClassType)^.IntfGuid;
  50735. // check that all interfaces are implemented by this class
  50736. if (aSharedImplementation<>nil) and
  50737. aSharedImplementation.InheritsFrom(TInterfacedObjectFake) then begin
  50738. if IsEqualGUID(UID[0]^,TInterfacedObjectFake(aSharedImplementation).
  50739. fFactory.fInterfaceIID) then
  50740. UID[0] := nil; // mark TGUID implemented by this fake interface
  50741. end else begin
  50742. C := aImplementationClass; // search all implemented TGUID for this class
  50743. repeat
  50744. T := C.GetInterfaceTable;
  50745. if T<>nil then
  50746. for i := 0 to T^.EntryCount-1 do
  50747. with T^.Entries[i] do
  50748. for j := 0 to high(aInterfaces) do
  50749. if (UID[j]<>nil) and IsEqualGUID(UID[j]^,IID{$ifdef FPC}^{$endif}) then begin
  50750. UID[j] := nil; // mark TGUID found
  50751. break;
  50752. end;
  50753. C := C.ClassParent;
  50754. until C=nil;
  50755. end;
  50756. for j := 0 to high(aInterfaces) do
  50757. if UID[j]<>nil then
  50758. raise EServiceException.CreateUTF8('%.AddImplementation: % not found in %',
  50759. [self,aInterfaces[j]^.Name,aImplementationClass]);
  50760. // register this implementation class
  50761. for j := 0 to high(aInterfaces) do begin
  50762. F := TServiceFactoryServer.Create(Rest as TSQLRestServer,aInterfaces[j],
  50763. aInstanceCreation,aImplementationClass,aContractExpected,1800,aSharedImplementation);
  50764. if result=nil then begin
  50765. result := F; // returns the first registered interface
  50766. if (aInstanceCreation=sicShared) and (aSharedImplementation=nil) then
  50767. aSharedImplementation := F.fSharedInstance; // re-use existing instance
  50768. end;
  50769. AddServiceInternal(F);
  50770. end;
  50771. end;
  50772. procedure TServiceContainerServer.OnCloseSession(aSessionID: cardinal);
  50773. var i: Integer;
  50774. Inst: TServiceFactoryServerInstance;
  50775. begin
  50776. Inst.InstanceID := aSessionID;
  50777. for i := 0 to Count-1 do
  50778. with TServiceFactoryServer(Index(i)) do
  50779. if InstanceCreation=sicPerSession then
  50780. InternalInstanceRetrieve(Inst,SERVICE_METHODINDEX_FREEINSTANCE);
  50781. end;
  50782. destructor TServiceContainerServer.Destroy;
  50783. var i: integer;
  50784. begin
  50785. if fFakeCallbacks<>nil then begin
  50786. for i := 0 to fFakeCallbacks.Count-1 do // prevent GPF in Destroy
  50787. TInterfacedObjectFakeServer(fFakeCallbacks.List[i]).fServer := nil;
  50788. FreeAndNil(fFakeCallbacks); // do not own objects
  50789. end;
  50790. fRecordVersionCallback := nil; // to be done after fFakeCallbacks[].fServer := nil
  50791. inherited Destroy;
  50792. end;
  50793. procedure TServiceContainerServer.FakeCallbackAdd(aFakeInstance: TObject);
  50794. begin
  50795. if self=nil then
  50796. exit;
  50797. if fFakeCallbacks=nil then
  50798. fFakeCallbacks := TObjectListLocked.Create(false);
  50799. fFakeCallbacks.Safe.Lock;
  50800. fFakeCallbacks.Add(aFakeInstance);
  50801. fFakeCallbacks.Safe.UnLock;
  50802. end;
  50803. procedure TServiceContainerServer.FakeCallbackRemove(aFakeInstance: TObject);
  50804. var i,callbackID: integer;
  50805. connectionID: Int64;
  50806. fake: TInterfacedObjectFakeServer;
  50807. server: TSQLRestServer;
  50808. begin
  50809. if (self=nil) or (fFakeCallbacks=nil) then
  50810. exit;
  50811. connectionID := 0;
  50812. callbackID := 0;
  50813. fFakeCallbacks.Safe.Lock;
  50814. try
  50815. i := fFakeCallbacks.IndexOf(aFakeInstance);
  50816. if i>=0 then begin
  50817. fake := fFakeCallbacks.List[i];
  50818. if not fake.fReleasedOnClientSide then begin
  50819. connectionID := fake.fLowLevelConnectionID;
  50820. callbackID := fake.ClientDrivenID;
  50821. if Assigned(OnCallbackReleasedOnServerSide) then
  50822. OnCallbackReleasedOnServerSide(self,fake,fake.fFakeInterface);
  50823. end;
  50824. fFakeCallbacks.Delete(i);
  50825. end;
  50826. finally
  50827. fFakeCallbacks.Safe.UnLock;
  50828. end;
  50829. if connectionID<>0 then begin
  50830. server := fRest as TSQLRestServer;
  50831. if Assigned(server.OnNotifyCallback) then
  50832. server.OnNotifyCallback(server,SERVICE_PSEUDO_METHOD[imFree],'',
  50833. connectionID,callbackID,nil,nil);
  50834. end;
  50835. end;
  50836. procedure TServiceContainerServer.FakeCallbackRelease(Ctxt: TSQLRestServerURIContext);
  50837. var i: integer;
  50838. fake: TInterfacedObjectFakeServer;
  50839. connectionID: Int64;
  50840. fakeID: PtrUInt;
  50841. Values: TNameValuePUTF8CharDynArray;
  50842. withLog: boolean; // avoid stack overflow
  50843. begin
  50844. if (self=nil) or (fFakeCallbacks=nil) or (Ctxt=nil) then
  50845. exit;
  50846. connectionID := Ctxt.Call^.LowLevelConnectionID;
  50847. JSONDecode(pointer(Ctxt.Call^.InBody),Values);
  50848. if length(Values)<>1 then
  50849. exit;
  50850. fakeID := GetCardinal(Values[0].Value);
  50851. if (fakeID=0) or (connectionID=0) or (Values[0].Name=nil) then
  50852. exit;
  50853. withLog := not IdemPropNameU('ISynLogCallback',Values[0].Name,StrLen(Values[0].Name));
  50854. if withLog then // avoid stack overflow ;)
  50855. fRest.InternalLog('%.FakeCallbackRelease(%,"%") remote call',
  50856. [ClassType,fakeID,Values[0].Name],sllDebug);
  50857. try
  50858. fFakeCallbacks.Safe.Lock;
  50859. for i := 0 to fFakeCallbacks.Count-1 do begin
  50860. fake := fFakeCallbacks.List[i];
  50861. if (fake.fLowLevelConnectionID=connectionID) and
  50862. (fake.ClientDrivenID=fakeID) then begin
  50863. fake.fReleasedOnClientSide := true;
  50864. if Assigned(OnCallbackReleasedOnClientSide) then
  50865. OnCallbackReleasedOnClientSide(self,fake,fake.fFakeInterface);
  50866. if fake.fService.fInterface.MethodIndexCallbackReleased>=0 then begin
  50867. // emulate a call to CallbackReleased(callback,'ICallbackName')
  50868. Ctxt.ServiceMethodIndex := fake.fService.fInterface.MethodIndexCallbackReleased;
  50869. Ctxt.ServiceExecution := @fake.fService.fExecution[Ctxt.ServiceMethodIndex];
  50870. Ctxt.Service := fake.fService;
  50871. fake._AddRef; // IInvokable=pointer in Ctxt.ExecuteCallback
  50872. Ctxt.ServiceParameters := pointer(FormatUTF8('[%,"%"]',
  50873. [PtrInt(fake.fFakeInterface),Values[0].Name]));
  50874. fake.fService.ExecuteMethod(Ctxt);
  50875. if withLog then
  50876. fRest.InternalLog('I%() returned %',[Ctxt.Service.fInterface.
  50877. Methods[Ctxt.ServiceMethodIndex].InterfaceDotMethodName,
  50878. Ctxt.Call^.OutStatus],sllDebug);
  50879. end else
  50880. Ctxt.Success;
  50881. exit;
  50882. end;
  50883. end;
  50884. finally
  50885. fFakeCallbacks.Safe.UnLock;
  50886. end;
  50887. end;
  50888. function TServiceContainerServer.RecordVersionSynchronizeSubscribeMaster(
  50889. TableIndex: integer; RecordVersion: TRecordVersion;
  50890. const SlaveCallback: IServiceRecordVersionCallback): boolean;
  50891. var instance: TObject;
  50892. begin
  50893. result := false;
  50894. if (self=nil) or (cardinal(TableIndex)>cardinal(fRest.Model.TablesMax)) then
  50895. exit;
  50896. fRest.fAcquireExecution[execORMWrite].fSafe.Lock;
  50897. try
  50898. if RecordVersion<>(fRest as TSQLRestServer).fRecordVersionMax then
  50899. exit; // there are some missing items on the client side
  50900. if fRecordVersionCallback=nil then
  50901. SetLength(fRecordVersionCallback,fRest.Model.TablesMax+1);
  50902. InterfaceArrayAdd(fRecordVersionCallback[TableIndex],SlaveCallback);
  50903. instance := ObjectFromInterface(SlaveCallback);
  50904. if (instance<>nil) and
  50905. (instance.ClassType=TInterfacedObjectFakeServer) then
  50906. TInterfacedObjectFakeServer(instance).fRaiseExceptionOnInvokeError := True;
  50907. finally
  50908. fRest.fAcquireExecution[execORMWrite].Safe.UnLock;
  50909. end;
  50910. result := true;
  50911. end;
  50912. class function TServiceContainerServer.CallbackReleasedOnClientSide(
  50913. const callback: IInterface): boolean;
  50914. var instance: TObject;
  50915. begin
  50916. instance := ObjectFromInterface(callback);
  50917. result := (instance<>nil) and
  50918. (instance.ClassType=TInterfacedObjectFakeServer) and
  50919. TInterfacedObjectFakeServer(instance).fReleasedOnClientSide;
  50920. end;
  50921. procedure TServiceContainerServer.RecordVersionCallbackNotify(TableIndex: integer;
  50922. Occasion: TSQLOccasion; const DeletedID: TID; const DeletedRevision: TRecordVersion;
  50923. const AddUpdateJson: RawUTF8);
  50924. var i: integer;
  50925. arr: ^IServiceRecordVersionCallbackDynArray;
  50926. begin
  50927. try
  50928. fRest.fAcquireExecution[execORMWrite].fSafe.Lock;
  50929. try
  50930. arr := @fRecordVersionCallback[TableIndex];
  50931. for i := length(arr^)-1 downto 0 do // downto: InterfaceArrayDelete() below
  50932. if CallbackReleasedOnClientSide(arr^[i]) then
  50933. // automatic removal of any released callback
  50934. InterfaceArrayDelete(arr^,i) else
  50935. try
  50936. case Occasion of
  50937. soInsert: arr^[i].Added(AddUpdateJson);
  50938. soUpdate: arr^[i].Updated(AddUpdateJson);
  50939. soDelete: arr^[i].Deleted(DeletedID,DeletedRevision);
  50940. end;
  50941. except // on notification error -> delete this entry
  50942. InterfaceArrayDelete(arr^,i);
  50943. end;
  50944. finally
  50945. fRest.fAcquireExecution[execORMWrite].Safe.UnLock;
  50946. end;
  50947. except // ignore any exception here
  50948. end;
  50949. end;
  50950. procedure TServiceContainerServer.RecordVersionNotifyAddUpdate(
  50951. Occasion: TSQLOccasion; TableIndex: integer; const Document: TDocVariantData);
  50952. var json: RawUTF8;
  50953. begin
  50954. if (Occasion in [soInsert,soUpdate]) and
  50955. (fRecordVersionCallback<>nil) and
  50956. (fRecordVersionCallback[TableIndex]<>nil) then begin
  50957. json := Document.ToJSON;
  50958. RecordVersionCallbackNotify(TableIndex,Occasion,0,0,json);
  50959. end;
  50960. end;
  50961. procedure TServiceContainerServer.RecordVersionNotifyAddUpdate(
  50962. Occasion: TSQLOccasion; TableIndex: integer; const Decoder: TJSONObjectDecoder);
  50963. var json: RawUTF8;
  50964. begin
  50965. if (Occasion in [soInsert,soUpdate]) and
  50966. (fRecordVersionCallback<>nil) and
  50967. (fRecordVersionCallback[TableIndex]<>nil) then begin
  50968. Decoder.EncodeAsJSON(json);
  50969. RecordVersionCallbackNotify(TableIndex,Occasion,0,0,json);
  50970. end;
  50971. end;
  50972. procedure TServiceContainerServer.RecordVersionNotifyDelete(
  50973. TableIndex: integer; const ID: TID; const Revision: TRecordVersion);
  50974. begin
  50975. if (fRecordVersionCallback<>nil) and
  50976. (fRecordVersionCallback[TableIndex]<>nil) then
  50977. RecordVersionCallbackNotify(TableIndex,soDelete,ID,Revision,'');
  50978. end;
  50979. procedure TServiceContainerServer.SetServiceLog(aLogRest: TSQLRest;
  50980. aLogClass: TSQLRecordServiceLogClass; const aExcludedMethodNamesCSV: RawUTF8);
  50981. var i,n: integer;
  50982. fact: TServiceFactory;
  50983. excluded: TServiceContainerInterfaceMethodBits;
  50984. methods: TInterfaceFactoryMethodBits;
  50985. somemethods: boolean;
  50986. begin
  50987. somemethods := aExcludedMethodNamesCSV<>'';
  50988. if somemethods then
  50989. SetInterfaceMethodBits(pointer(aExcludedMethodNamesCSV),true,excluded) else
  50990. FillcharFast(methods,sizeof(methods),255);
  50991. n := fListInterfaceMethods.Count;
  50992. i := 0;
  50993. while i<n do begin
  50994. fact := fListInterfaceMethod[i].InterfaceService;
  50995. if somemethods then begin
  50996. FillcharFast(methods,sizeof(methods),0);
  50997. somemethods := false;
  50998. end;
  50999. repeat
  51000. if (aExcludedMethodNamesCSV<>'') and not (i in excluded) then begin
  51001. include(methods,fListInterfaceMethod[i].
  51002. InterfaceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT);
  51003. somemethods := true;
  51004. end;
  51005. inc(i);
  51006. until (i>=n) or (fListInterfaceMethod[i].InterfaceService<>fact);
  51007. if (aExcludedMethodNamesCSV='') or somemethods then
  51008. TServiceFactoryServer(fact).SetServiceLogByIndex(methods,aLogRest,aLogClass);
  51009. end;
  51010. end;
  51011. { TServiceFactoryServer }
  51012. type
  51013. PCallMethodArgs = ^TCallMethodArgs;
  51014. {$ifdef FPC}
  51015. {$PACKRECORDS 16}
  51016. {$endif}
  51017. TCallMethodArgs = record
  51018. StackSize: integer;
  51019. StackAddr, method: PtrInt;
  51020. ParamRegs: packed array[PARAMREG_FIRST..PARAMREG_LAST] of PtrInt;
  51021. {$ifdef HAS_FPREG}
  51022. FPRegs: packed array[FPREG_FIRST..FPREG_LAST] of Double;
  51023. {$endif}
  51024. res64: Int64Rec;
  51025. resKind: TServiceMethodValueType;
  51026. end;
  51027. {$ifdef FPC}
  51028. {$PACKRECORDS DEFAULT}
  51029. {$endif}
  51030. procedure CallMethod(var Args: TCallMethodArgs);
  51031. // ARM/AARCH64 code below provided by ALF, greatly inspired by pascalscript
  51032. {$ifdef CPUARM}
  51033. assembler; nostackframe;
  51034. label stack_loop,load_regs,asmcall_end,float_result;
  51035. asm
  51036. //name r#(normally, darwin can differ)
  51037. //a1 0 argument 1 / integer result / scratch register
  51038. //a2 1 argument 2 / scratch register
  51039. //a3 2 argument 3 / scratch register
  51040. //a4 3 argument 4 / scratch register
  51041. //v1 4 register variable
  51042. //v2 5 register variable
  51043. //v3 6 register variable
  51044. //v4 7 register variable
  51045. //v5 8 register variable
  51046. //sb 9 static base / register variable
  51047. //sl 10 stack limit / stack chunk handle / reg. variable
  51048. //fp 11 frame pointer
  51049. //ip 12 scratch register / new-sb in inter-link-unit calls
  51050. //sp 13 lower end of current stack frame
  51051. //lr 14 link address / scratch register
  51052. //pc 15 program counter
  51053. // prolog
  51054. mov ip, sp // sp is the stack pointer ; ip is the Intra-Procedure-call scratch register
  51055. stmfd sp!, {v1, v2, sb, sl, fp, ip, lr, pc}
  51056. sub fp, ip, #4
  51057. // make space on stack
  51058. sub sp, sp, #MAX_EXECSTACK
  51059. mov v2, Args
  51060. // copy (push) stack content (if any)
  51061. ldr a1, [v2,#TCallMethodArgs.StackSize]
  51062. // if there is no stack content, do nothing
  51063. cmp a1, #0
  51064. beq load_regs
  51065. // point a2 to bottom of stack.
  51066. mov a2, sp
  51067. // load a3 with CallMethod stack address
  51068. ldr a3, [v2,#TCallMethodArgs.StackAddr]
  51069. stack_loop:
  51070. // copy a3 to a4 and increment a3 (a3 = StackAddr)
  51071. ldmia a3!, {a4}
  51072. // copy a4 to a2 and increment a2 (a2 = StackPointer)
  51073. stmia a2!, {a4}
  51074. // decrement stacksize counter, with update of flags for loop
  51075. subs a1, a1, #1
  51076. bne stack_loop
  51077. load_regs:
  51078. ldr r0, [v2,#TCallMethodArgs.ParamRegs+REGR0*4-4]
  51079. ldr r1, [v2,#TCallMethodArgs.ParamRegs+REGR1*4-4]
  51080. ldr r2, [v2,#TCallMethodArgs.ParamRegs+REGR2*4-4]
  51081. ldr r3, [v2,#TCallMethodArgs.ParamRegs+REGR3*4-4]
  51082. vldr d0, [v2,#TCallMethodArgs.FPRegs+REGD0*8-8]
  51083. vldr d1, [v2,#TCallMethodArgs.FPRegs+REGD1*8-8]
  51084. vldr d2, [v2,#TCallMethodArgs.FPRegs+REGD2*8-8]
  51085. vldr d3, [v2,#TCallMethodArgs.FPRegs+REGD3*8-8]
  51086. vldr d4, [v2,#TCallMethodArgs.FPRegs+REGD4*8-8]
  51087. vldr d5, [v2,#TCallMethodArgs.FPRegs+REGD5*8-8]
  51088. vldr d6, [v2,#TCallMethodArgs.FPRegs+REGD6*8-8]
  51089. vldr d7, [v2,#TCallMethodArgs.FPRegs+REGD7*8-8]
  51090. ldr v1, [v2,#TCallMethodArgs.method]
  51091. blx v1
  51092. str a1, [v2,#TCallMethodArgs.res64.Lo]
  51093. str a2, [v2,#TCallMethodArgs.res64.Hi]
  51094. ldr a3, [v2,#TCallMethodArgs.resKind]
  51095. cmp a3, smvDouble
  51096. beq float_result
  51097. cmp a3, smvDateTime
  51098. beq float_result
  51099. cmp a3, smvCurrency
  51100. bne asmcall_end
  51101. // store double result in res64
  51102. float_result:
  51103. vstr d0, [v2,#TCallMethodArgs.res64]
  51104. asmcall_end:
  51105. // epilog
  51106. ldmea fp, {v1, v2, sb, sl, fp, sp, pc}
  51107. end;
  51108. {$endif CPUARM}
  51109. {$ifdef CPUAARCH64}
  51110. assembler; nostackframe;
  51111. label stack_loop,load_regs,asmcall_end,float_result;
  51112. asm
  51113. // inspired by pascal script
  51114. // fp x29
  51115. // lr x30
  51116. // sp sp
  51117. stp fp, lr, [sp, #-16]!
  51118. stp x19, x20, [sp, #-16]!
  51119. mov fp, sp
  51120. // make space on stack
  51121. sub sp, sp, #MAX_EXECSTACK
  51122. mov x19, Args
  51123. ldr x20, [x19,#TCallMethodArgs.method]
  51124. // prepare to copy (push) stack content (if any)
  51125. ldr x2, [x19,#TCallMethodArgs.StackSize]
  51126. // if there is no stack content, do nothing
  51127. cmp x2, #0
  51128. b.eq load_regs
  51129. // point x3 to bottom of stack.
  51130. mov x3, sp
  51131. // load x4 with CallMethod stack address
  51132. ldr x4, [x19,#TCallMethodArgs.StackAddr]
  51133. stack_loop:
  51134. // load x5 and x6 with stack contents
  51135. ldr x5, [x4]
  51136. ldr x6, [x4,#8]
  51137. // store contents at "real" stack and increment address counter x3
  51138. stp x5, x6, [x3], #16
  51139. // with update of flags for loop
  51140. // (mandatory: stacksize must be a multiple of 2 [16 bytes] !!)
  51141. // inc stackaddr counter by 16 (2 registers are pushed every loop)
  51142. add x4, x4, #16
  51143. // decrement stacksize counter by 2 (2 registers are pushed every loop),
  51144. // with update of flags for loop
  51145. subs x2, x2, #2
  51146. b.ne stack_loop
  51147. load_regs:
  51148. ldr x0, [x19,#TCallMethodArgs.ParamRegs+REGX0*8-8]
  51149. ldr x1, [x19,#TCallMethodArgs.ParamRegs+REGX1*8-8]
  51150. ldr x2, [x19,#TCallMethodArgs.ParamRegs+REGX2*8-8]
  51151. ldr x3, [x19,#TCallMethodArgs.ParamRegs+REGX3*8-8]
  51152. ldr x4, [x19,#TCallMethodArgs.ParamRegs+REGX4*8-8]
  51153. ldr x5, [x19,#TCallMethodArgs.ParamRegs+REGX5*8-8]
  51154. ldr x6, [x19,#TCallMethodArgs.ParamRegs+REGX6*8-8]
  51155. ldr x7, [x19,#TCallMethodArgs.ParamRegs+REGX7*8-8]
  51156. ldr d0, [x19,#TCallMethodArgs.FPRegs+REGD0*8-8]
  51157. ldr d1, [x19,#TCallMethodArgs.FPRegs+REGD1*8-8]
  51158. ldr d2, [x19,#TCallMethodArgs.FPRegs+REGD2*8-8]
  51159. ldr d3, [x19,#TCallMethodArgs.FPRegs+REGD3*8-8]
  51160. ldr d4, [x19,#TCallMethodArgs.FPRegs+REGD4*8-8]
  51161. ldr d5, [x19,#TCallMethodArgs.FPRegs+REGD5*8-8]
  51162. ldr d6, [x19,#TCallMethodArgs.FPRegs+REGD6*8-8]
  51163. ldr d7, [x19,#TCallMethodArgs.FPRegs+REGD7*8-8]
  51164. // call TCallMethodArgs.method
  51165. blr x20
  51166. // store normal result
  51167. str x0, [x19, #TCallMethodArgs.res64]
  51168. ldr x20, [x19, #TCallMethodArgs.resKind]
  51169. cmp x20, smvDouble
  51170. b.eq float_result
  51171. cmp x20, smvDateTime
  51172. b.eq float_result
  51173. cmp x20, smvCurrency
  51174. b.ne asmcall_end
  51175. // store double result in res64
  51176. float_result:
  51177. str d0, [x19,#TCallMethodArgs.res64]
  51178. asmcall_end:
  51179. // give back space on stack (add sp,sp,#MAX_EXECSTACK)
  51180. mov sp, fp
  51181. ldp x19, x20, [sp], #16
  51182. ldp fp, lr, [sp], #16
  51183. ret
  51184. end;
  51185. {$endif CPUAARCH64}
  51186. {$ifdef CPUX64} assembler;
  51187. {$ifdef FPC}
  51188. nostackframe;
  51189. asm
  51190. push rbp
  51191. push r12
  51192. mov rbp,rsp
  51193. // simulate .params 60 ... size for 60 parameters
  51194. lea rsp,[rsp-MAX_EXECSTACK]
  51195. // align stack
  51196. and rsp, -16
  51197. {$else DELPHI} // ensure we use regular .params command for easier debugging
  51198. asm
  51199. .params 64 // size for 64 parameters
  51200. .pushnv r12 // generate prolog+epilog to save and restore non-volatile r12
  51201. {$endif FPC}
  51202. // get Args
  51203. mov r12, Args
  51204. // copy (push) stack content (if any)
  51205. mov ecx, [r12].TCallMethodArgs.StackSize
  51206. mov rdx, [r12].TCallMethodArgs.StackAddr
  51207. jmp @checkstack
  51208. @addstack:
  51209. push qword ptr [rdx]
  51210. dec ecx
  51211. sub rdx,8
  51212. @checkstack:
  51213. or ecx, ecx
  51214. jnz @addstack
  51215. // fill registers and call method
  51216. {$ifdef LINUX}
  51217. // Linux/BSD System V AMD64 ABI
  51218. mov rdi, [r12+TCallMethodArgs.ParamRegs+REGRDI*8-8]
  51219. mov rsi, [r12+TCallMethodArgs.ParamRegs+REGRSI*8-8]
  51220. mov rdx, [r12+TCallMethodArgs.ParamRegs+REGRDX *8-8]
  51221. mov rcx, [r12+TCallMethodArgs.ParamRegs+REGRCX *8-8]
  51222. mov r8, [r12+TCallMethodArgs.ParamRegs+REGR8*8-8]
  51223. mov r9, [r12+TCallMethodArgs.ParamRegs+REGR9*8-8]
  51224. movsd xmm0, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM0*8-8]
  51225. movsd xmm1, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM1*8-8]
  51226. movsd xmm2, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM2*8-8]
  51227. movsd xmm3, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM3*8-8]
  51228. movsd xmm4, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM4*8-8]
  51229. movsd xmm5, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM5*8-8]
  51230. movsd xmm6, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM6*8-8]
  51231. movsd xmm7, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM7*8-8]
  51232. call [r12].TCallMethodArgs.method
  51233. {$else}
  51234. // Win64 ABI
  51235. mov rcx, [r12+TCallMethodArgs.ParamRegs+REGRCX*8-8]
  51236. mov rdx, [r12+TCallMethodArgs.ParamRegs+REGRDX*8-8]
  51237. mov r8, [r12+TCallMethodArgs.ParamRegs+REGR8 *8-8]
  51238. mov r9, [r12+TCallMethodArgs.ParamRegs+REGR9 *8-8]
  51239. movsd xmm0, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM0*8-8]
  51240. movsd xmm1, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM1*8-8]
  51241. movsd xmm2, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM2*8-8]
  51242. movsd xmm3, qword ptr [r12+TCallMethodArgs.FPRegs+REGXMM3*8-8]
  51243. sub rsp,8*4 // reserve shadow-space for RCX,RDX,R8,R9 registers
  51244. call [r12].TCallMethodArgs.method
  51245. add rsp,8*4
  51246. {$endif LINUX}
  51247. // retrieve result
  51248. mov [r12].TCallMethodArgs.res64, rax
  51249. mov cl, [r12].TCallMethodArgs.resKind
  51250. cmp cl, smvDouble
  51251. je @d
  51252. cmp cl, smvDateTime
  51253. je @d
  51254. cmp cl, smvCurrency
  51255. jne @e
  51256. @d: movlpd qword ptr [r12].TCallMethodArgs.res64, xmm0
  51257. @e:
  51258. {$ifdef FPC}
  51259. mov rsp, rbp
  51260. pop r12
  51261. pop rbp
  51262. {$endif}
  51263. end;
  51264. {$endif CPUX64}
  51265. {$ifdef CPUX86}
  51266. asm
  51267. push esi
  51268. push ebp
  51269. mov ebp,esp
  51270. mov esi,Args
  51271. // copy stack content (if any)
  51272. mov eax,[esi].TCallMethodArgs.StackSize
  51273. mov edx,dword ptr [esi].TCallMethodArgs.StackAddr
  51274. add edx,eax // pascal/register convention = left-to-right
  51275. shr eax,2
  51276. jz @z
  51277. @n: sub edx,4
  51278. mov ecx,[edx]
  51279. push ecx
  51280. dec eax
  51281. jnz @n
  51282. // call method
  51283. @z: mov eax,[esi+TCallMethodArgs.ParamRegs+REGEAX*4-4]
  51284. mov edx,[esi+TCallMethodArgs.ParamRegs+REGEDX*4-4]
  51285. mov ecx,[esi+TCallMethodArgs.ParamRegs+REGECX*4-4]
  51286. call [esi].TCallMethodArgs.method
  51287. // retrieve result
  51288. mov cl,[esi].TCallMethodArgs.resKind
  51289. cmp cl,smvDouble
  51290. je @d
  51291. cmp cl,smvDateTime
  51292. je @d
  51293. cmp cl,smvCurrency
  51294. jne @i
  51295. fistp qword [esi].TCallMethodArgs.res64
  51296. jmp @e
  51297. @d: fstp qword [esi].TCallMethodArgs.res64
  51298. jmp @e
  51299. @i: mov [esi].TCallMethodArgs.res64.Lo,eax
  51300. mov [esi].TCallMethodArgs.res64.Hi,edx
  51301. @e: mov esp,ebp
  51302. pop ebp
  51303. pop esi
  51304. end;
  51305. {$endif CPUX86}
  51306. procedure BackgroundExecuteProc(Call: pointer);
  51307. var synch: PBackgroundLauncher absolute Call;
  51308. threadContext: PServiceRunningContext;
  51309. backup: TServiceRunningContext;
  51310. begin
  51311. threadContext := @ServiceContext; // faster to use a pointer than GetTls()
  51312. backup := threadContext^;
  51313. threadContext^.Factory := synch^.Context^.Factory;
  51314. threadContext^.Request := synch^.Context^.Request;
  51315. try
  51316. case synch^.Action of
  51317. doCallMethod:
  51318. CallMethod(PCallMethodArgs(synch^.CallMethodArgs)^);
  51319. doInstanceRelease:
  51320. synch^.Instance.InternalRelease;
  51321. doThreadMethod:
  51322. synch^.ThreadMethod;
  51323. end;
  51324. finally
  51325. threadContext^ := backup;
  51326. end;
  51327. end;
  51328. constructor TServiceFactoryServer.Create(aRestServer: TSQLRestServer; aInterface: PTypeInfo;
  51329. aInstanceCreation: TServiceInstanceImplementation;
  51330. aImplementationClass: TInterfacedClass; const aContractExpected: RawUTF8;
  51331. aTimeOutSec: cardinal; aSharedInstance: TInterfacedObject);
  51332. begin
  51333. // extract RTTI from the interface
  51334. if aInstanceCreation<>sicPerThread then
  51335. InitializeCriticalSection(fInstanceLock);
  51336. inherited Create(aRestServer,aInterface,aInstanceCreation,aContractExpected);
  51337. if fRest.MethodAddress(ShortString(InterfaceURI))<>nil then
  51338. raise EServiceException.CreateUTF8('%.Create: I% already exposed as % published method',
  51339. [self,InterfaceURI,fRest]) else
  51340. fImplementationClass := aImplementationClass;
  51341. if fImplementationClass.InheritsFrom(TInterfacedObjectFake) then begin
  51342. fImplementationClassKind := ickFake;
  51343. if aSharedInstance=nil then
  51344. raise EServiceException.CreateUTF8('%.Create: no Shared Instance for %/I%',
  51345. [self,fImplementationClass,fInterfaceURI]);
  51346. if (aSharedInstance as TInterfacedObjectFake).Factory.fInterfaceTypeInfo<>aInterface then
  51347. raise EServiceException.CreateUTF8('%.Create: shared % instance does not implement I%',
  51348. [self,fImplementationClass,fInterfaceURI]) else
  51349. end else begin
  51350. if aRestServer.Services.Implements(fInterface.fInterfaceTypeInfo) then
  51351. fImplementationClassKind := ickFromInjectedResolver else
  51352. if fImplementationClass.InheritsFrom(TInjectableObjectRest) then
  51353. fImplementationClassKind := ickInjectableRest else
  51354. if fImplementationClass.InheritsFrom(TInjectableObject) then
  51355. fImplementationClassKind := ickInjectable else
  51356. if fImplementationClass.InheritsFrom(TInterfacedObjectWithCustomCreate) then
  51357. fImplementationClassKind := ickWithCustomCreate;
  51358. fImplementationClassInterfaceEntry :=
  51359. fImplementationClass.GetInterfaceEntry(fInterface.fInterfaceIID);
  51360. if fImplementationClassInterfaceEntry=nil then
  51361. raise EServiceException.CreateUTF8('%.Create: % does not implement I%',
  51362. [self,fImplementationClass,fInterfaceURI]) else
  51363. end;
  51364. if (fInterface.MethodIndexCallbackReleased>=0) and
  51365. (InstanceCreation<>sicShared) then
  51366. raise EServiceException.CreateUTF8('%.Create: I%() should be run as sicShared',
  51367. [self,fInterface.fMethods[fInterface.MethodIndexCallbackReleased].
  51368. InterfaceDotMethodName]);
  51369. // initialize the shared instance or client driven parameters
  51370. case InstanceCreation of
  51371. sicShared: begin
  51372. if aSharedInstance=nil then
  51373. fSharedInstance := CreateInstance(false) else
  51374. if aSharedInstance.InheritsFrom(fImplementationClass) then
  51375. fSharedInstance := aSharedInstance else
  51376. raise EServiceException.CreateUTF8('%.Create: % shared instance '+
  51377. 'does not inherit from %',[self,aSharedInstance,fImplementationClass]);
  51378. if fImplementationClassKind<>ickFake then
  51379. if (fSharedInstance=nil) or
  51380. not GetInterfaceFromEntry(
  51381. fSharedInstance,fImplementationClassInterfaceEntry,fSharedInterface) then
  51382. raise EServiceException.CreateUTF8('%.Create: % is no implementation of I%',
  51383. [self,fSharedInstance,fInterfaceURI]);
  51384. end;
  51385. sicClientDriven, sicPerSession, sicPerUser, sicPerGroup, sicPerThread:
  51386. if (aTimeOutSec=0) and (InstanceCreation<>sicPerThread) then
  51387. fInstanceCreation := sicSingle else begin
  51388. // only instances list is protected, since client calls shall be pipelined
  51389. fInstance.InitSpecific(TypeInfo(TServiceFactoryServerInstanceDynArray),
  51390. fInstances,djCardinal,@fInstancesCount); // sort by InstanceID: cardinal
  51391. fInstanceTimeOut := aTimeOutSec*1000;
  51392. end;
  51393. end;
  51394. SetLength(fStats,fInterface.MethodsCount);
  51395. end;
  51396. procedure TServiceFactoryServer.SetTimeoutSecInt(value: cardinal);
  51397. begin
  51398. if (self=nil) or not (InstanceCreation in [
  51399. sicClientDriven,sicPerSession,sicPerUser,sicPerGroup,sicPerThread]) then
  51400. raise EServiceException.CreateUTF8('%.SetTimeoutSecInt() with %',
  51401. [self,ToText(InstanceCreation)^]);
  51402. fInstanceTimeOut := value*1000;
  51403. end;
  51404. function TServiceFactoryServer.GetTimeoutSec: cardinal;
  51405. begin
  51406. if (self=nil) or not (InstanceCreation in [
  51407. sicClientDriven,sicPerSession,sicPerUser,sicPerGroup,sicPerThread]) then
  51408. result := 0 else
  51409. result := fInstanceTimeout div 1000;
  51410. end;
  51411. function TServiceFactoryServer.GetStat(const aMethod: RawUTF8): TSynMonitorInputOutput;
  51412. begin
  51413. result := fStats[fInterface.CheckMethodIndex(aMethod)];
  51414. end;
  51415. destructor TServiceFactoryServer.Destroy;
  51416. var i: integer;
  51417. begin
  51418. try
  51419. for i := 0 to High(fLogRestBatch) do begin
  51420. with fLogRestBatch[i] do begin
  51421. Safe.Lock;
  51422. try
  51423. if Count>0 then
  51424. Rest.BatchSend(fLogRestBatch[i]);
  51425. finally
  51426. Safe.Unlock;
  51427. end;
  51428. end;
  51429. FreeAndNil(fLogRestBatch[i]);
  51430. end;
  51431. if InstanceCreation<>sicPerThread then
  51432. EnterCriticalSection(fInstanceLock);
  51433. try // release any internal instance (should have been done by client)
  51434. try
  51435. for i := 0 to fInstancesCount-1 do
  51436. if fInstances[i].Instance<>nil then
  51437. fInstances[i].SafeFreeInstance(self);
  51438. finally
  51439. {$ifndef LVCL}
  51440. FreeAndNil(fBackgroundThread);
  51441. {$endif}
  51442. end;
  51443. except
  51444. ; // better ignore any error in business logic code
  51445. end;
  51446. finally
  51447. if InstanceCreation<>sicPerThread then
  51448. LeaveCriticalSection(fInstanceLock);
  51449. end;
  51450. if InstanceCreation<>sicPerThread then
  51451. DeleteCriticalSection(fInstanceLock);
  51452. ObjArrayClear(fStats);
  51453. inherited;
  51454. end;
  51455. function TServiceFactoryServer.Get(out Obj): Boolean;
  51456. var Inst: TServiceFactoryServerInstance;
  51457. begin
  51458. result := false;
  51459. if self=nil then
  51460. exit;
  51461. case fInstanceCreation of
  51462. sicShared:
  51463. if fSharedInterface<>nil then begin
  51464. IInterface(Obj) := fSharedInterface; // copy implementation interface
  51465. result := true;
  51466. end;
  51467. sicPerThread: begin
  51468. Inst.Instance := nil;
  51469. Inst.InstanceID := PtrUInt(GetCurrentThreadId);
  51470. if not InternalInstanceRetrieve(Inst,0) and (Inst.Instance<>nil) then
  51471. result := GetInterfaceFromEntry(Inst.Instance,fImplementationClassInterfaceEntry,Obj);
  51472. end;
  51473. else begin // no user/group/session on pure server-side -> always sicSingle
  51474. Inst.Instance := CreateInstance(false);
  51475. if Inst.Instance<>nil then
  51476. result := GetInterfaceFromEntry(Inst.Instance,fImplementationClassInterfaceEntry,Obj);
  51477. end;
  51478. end;
  51479. if result then
  51480. with PServiceRunningContext(@ServiceContext)^ do
  51481. if Factory=nil then
  51482. Factory := self;
  51483. end;
  51484. function TServiceFactoryServer.RetrieveSignature: RawUTF8;
  51485. begin
  51486. if self=nil then
  51487. result := '' else
  51488. result := Contract; // just return the current value
  51489. end;
  51490. procedure TServiceFactoryServerInstance.SafeFreeInstance(Factory: TServiceFactoryServer);
  51491. var Obj: TInterfacedObject;
  51492. begin
  51493. InstanceID := 0;
  51494. Obj := Instance;
  51495. Instance := nil;
  51496. {$ifndef LVCL}
  51497. if (optFreeInMainThread in Factory.fAnyOptions) and
  51498. (GetCurrentThreadID<>MainThreadID) then
  51499. BackgroundExecuteInstanceRelease(Obj,nil) else
  51500. {$endif}
  51501. if (optFreeInPerInterfaceThread in Factory.fAnyOptions) and
  51502. Assigned(Factory.fBackgroundThread) then
  51503. BackgroundExecuteInstanceRelease(Obj,Factory.fBackgroundThread) else
  51504. IInterface(Obj)._Release;
  51505. end;
  51506. function TServiceFactoryServer.InternalInstanceRetrieve(
  51507. var Inst: TServiceFactoryServerInstance; aMethodIndex: integer): boolean;
  51508. procedure AddNew;
  51509. var i: integer;
  51510. P: ^TServiceFactoryServerInstance;
  51511. begin
  51512. Inst.Instance := CreateInstance(true);
  51513. if Inst.Instance=nil then
  51514. exit;
  51515. fRest.InternalLog('%.InternalInstanceRetrieve: Adding % instance (id=%)',
  51516. [ClassType,fInterfaceURI,Inst.InstanceID],sllDebug);
  51517. P := pointer(fInstances);
  51518. for i := 1 to fInstancesCount do
  51519. if P^.InstanceID=0 then begin
  51520. P^ := Inst; // found an empty entry -> re-use it
  51521. exit;
  51522. end else
  51523. inc(P);
  51524. fInstance.Add(Inst); // append a new entry
  51525. end;
  51526. var i: integer;
  51527. begin
  51528. result := false;
  51529. if InstanceCreation<>sicPerThread then
  51530. EnterCriticalSection(fInstanceLock);
  51531. try
  51532. Inst.LastAccess64 := GetTickCount64;
  51533. // first release any deprecated instances
  51534. if fInstanceTimeout<>0 then
  51535. for i := fInstancesCount-1 downto 0 do
  51536. with fInstances[i] do
  51537. if InstanceID<>0 then
  51538. if Inst.LastAccess64>LastAccess64+fInstanceTimeout then begin
  51539. // deprecated -> mark this entry as empty
  51540. fRest.InternalLog(
  51541. '%.InternalInstanceRetrieve: Deleted % instance (id=%) after % ms timeout (max % ms)',
  51542. [ClassType,fInterfaceURI,InstanceID,Inst.LastAccess64-LastAccess64,fInstanceTimeOut],sllDebug);
  51543. SafeFreeInstance(self);
  51544. end;
  51545. if Inst.InstanceID=0 then begin
  51546. // retrieve or initialize a sicClientDriven instance
  51547. if (cardinal(aMethodIndex)>=fInterface.fMethodsCount) or
  51548. (InstanceCreation<>sicClientDriven) then
  51549. exit;
  51550. // initialize the new instance
  51551. inc(fInstanceCurrentID);
  51552. Inst.InstanceID := fInstanceCurrentID;
  51553. AddNew;
  51554. end else begin
  51555. // search the instance corresponding to Inst.InstanceID
  51556. for i := 0 to fInstancesCount-1 do
  51557. with fInstances[i] do
  51558. if InstanceID=Inst.InstanceID then begin
  51559. if aMethodIndex=SERVICE_METHODINDEX_FREEINSTANCE then begin
  51560. // aMethodIndex=-1 for {"method":"_free_", "params":[], "id":1234}
  51561. SafeFreeInstance(self);
  51562. result := true; // notify caller that successfully released instance
  51563. exit;
  51564. end;
  51565. LastAccess64 := Inst.LastAccess64;
  51566. Inst.Instance := Instance;
  51567. exit;
  51568. end;
  51569. // add any new session/user/group instance if necessary
  51570. if (InstanceCreation<>sicClientDriven) and
  51571. (cardinal(aMethodIndex)<fInterface.fMethodsCount) then
  51572. AddNew;
  51573. end;
  51574. finally
  51575. if InstanceCreation<>sicPerThread then
  51576. LeaveCriticalSection(fInstanceLock);
  51577. end;
  51578. end;
  51579. function TServiceFactoryServer.RestServer: TSQLRestServer;
  51580. begin
  51581. if self<>nil then
  51582. result := TSQLRestServer(fRest) else
  51583. result := nil;
  51584. end;
  51585. function TServiceFactoryServer.CreateInstance(AndIncreaseRefCount: boolean): TInterfacedObject;
  51586. var dummyObj: pointer;
  51587. begin
  51588. case fImplementationClassKind of
  51589. ickWithCustomCreate:
  51590. result := TInterfacedObjectWithCustomCreateClass(fImplementationClass).Create;
  51591. ickInjectable, ickInjectableRest: begin
  51592. result := TInjectableObjectClass(fImplementationClass).
  51593. CreateWithResolver(Rest.Services,true);
  51594. if fImplementationClassKind=ickInjectableRest then begin
  51595. TInjectableObjectRest(result).fFactory := self;
  51596. TInjectableObjectRest(result).fServer := RestServer;
  51597. end;
  51598. end;
  51599. ickFromInjectedResolver: begin
  51600. dummyObj := nil;
  51601. if not TSQLRestServer(Rest).Services.
  51602. TryResolveInternal(fInterface.fInterfaceTypeInfo,dummyObj) then
  51603. raise EInterfaceFactoryException.CreateUTF8(
  51604. 'ickFromInjectedResolver: TryResolveInternal(%)=false',
  51605. [fInterface.fInterfaceName]);
  51606. result := TInterfacedObject(ObjectFromInterface(IInterface(dummyObj)));
  51607. if AndIncreaseRefCount then // RefCount=1 after TryResolveInternal()
  51608. AndIncreaseRefCount := false else
  51609. dec(TInterfacedObjectHooked(result).FRefCount);
  51610. end;
  51611. else
  51612. result := fImplementationClass.Create;
  51613. end;
  51614. if Assigned(TSQLRestServer(Rest).OnServiceCreateInstance) then
  51615. TSQLRestServer(Rest).OnServiceCreateInstance(self,result);
  51616. if AndIncreaseRefCount then
  51617. IInterface(result)._AddRef; // allow passing self to sub-methods
  51618. end;
  51619. procedure TServiceFactoryServer.OnLogRestExecuteMethod(Sender: TServiceMethodExecute;
  51620. Step: TServiceMethodExecuteEventStep);
  51621. var W: TTextWriter;
  51622. a: integer;
  51623. begin
  51624. W := Sender.TempTextWriter;
  51625. with Sender.Method^ do
  51626. case Step of
  51627. smsBefore: begin
  51628. W.CancelAll;
  51629. W.AddShort('"POST",{Method:"');
  51630. W.AddString(InterfaceDotMethodName);
  51631. W.AddShort('",Input:{'); // as TSQLPropInfoRTTIVariant.GetJSONValues
  51632. if optNoLogInput in Sender.fOptions then
  51633. W.AddShort('optNoLogInput: true') else
  51634. for a := ArgsInFirst to ArgsInLast do
  51635. with Args[a] do
  51636. if (ValueDirection<>smdOut) and (ValueType<>smvInterface) then begin
  51637. W.AddShort(ParamName^); // in JSON_OPTIONS_FAST_EXTENDED format
  51638. W.Add(':');
  51639. AddJSON(W,Sender.Values[a]);
  51640. end;
  51641. W.CancelLastComma;
  51642. end;
  51643. smsAfter: begin
  51644. W.AddShort('},Output:{');
  51645. if optNoLogOutput in Sender.fOptions then
  51646. W.AddShort('optNoLogOutput: true') else
  51647. for a := ArgsOutFirst to ArgsOutLast do
  51648. with Args[a] do
  51649. if ValueDirection in [smdVar,smdOut,smdResult] then begin
  51650. W.AddShort(ParamName^);
  51651. W.Add(':');
  51652. AddJSON(W,Sender.Values[a]);
  51653. end;
  51654. W.CancelLastComma;
  51655. end;
  51656. smsError: begin
  51657. W.AddShort('},Output:{');
  51658. W.AddClassName(Sender.LastException.ClassType);
  51659. W.Add(':','"');
  51660. W.AddJSONEscapeString(Sender.LastException.Message);
  51661. W.Add('"');
  51662. end;
  51663. end;
  51664. end;
  51665. procedure TServiceFactoryServer.ExecuteMethod(Ctxt: TSQLRestServerURIContext);
  51666. var Inst: TServiceFactoryServerInstance;
  51667. WR: TJSONSerializer;
  51668. entry: PInterfaceEntry;
  51669. instancePtr: pointer;
  51670. dolock: boolean;
  51671. exec: TServiceMethodExecute;
  51672. timeStart,timeEnd: Int64;
  51673. stats: TSynMonitorInputOutput;
  51674. m: integer;
  51675. function GetFullMethodName: RawUTF8;
  51676. begin
  51677. if cardinal(Ctxt.ServiceMethodIndex)<fInterface.fMethodsCount then
  51678. result := fInterface.fMethods[Ctxt.ServiceMethodIndex].InterfaceDotMethodName else
  51679. result := fInterface.fInterfaceName;
  51680. end;
  51681. procedure Error(const Msg: RawUTF8; Status: integer=HTML_BADREQUEST);
  51682. begin
  51683. Ctxt.Error('(%) % for %',[ToText(InstanceCreation)^,Msg,GetFullMethodName],Status);
  51684. end;
  51685. function StatsCreate: TSynMonitorInputOutput;
  51686. begin
  51687. result := TSynMonitorInputOutput.Create(GetFullMethodName);
  51688. end;
  51689. procedure FinalizeLogRest;
  51690. var W: TTextWriter;
  51691. context: PServiceRunningContext;
  51692. begin
  51693. W := exec.TempTextWriter;
  51694. if exec.CurrentStep<smsBefore then begin
  51695. W.CancelAll;
  51696. W.Add('"POST",{Method:"%",Input:{',[exec.Method^.InterfaceDotMethodName]);
  51697. end;
  51698. if exec.CurrentStep<smsAfter then
  51699. W.AddShort('},Output:{Failed:"Probably due to wrong input"');
  51700. W.Add('},Session:%,User:%,Time:%,MicroSec:%},',
  51701. [integer(Ctxt.Session),Ctxt.SessionUser,TimeLogNowUTC,timeEnd]);
  51702. with Ctxt.ServiceExecution^ do
  51703. try
  51704. LogRestBatch.Safe.Lock;
  51705. LogRestBatch.RawAppend.AddNoJSONEscape(W);
  51706. if (LogRestBatch.Count>=500) or // write every second or after 500 rows
  51707. (GetTickCount64-LogRestBatch.ResetTix>1000) then begin
  51708. context := @ServiceContext;
  51709. context^.Request := nil; // avoid IsNotAllowed unexpected failure
  51710. try
  51711. LogRest.BatchSend(LogRestBatch);
  51712. LogRestBatch.Reset;
  51713. finally
  51714. context^.Request := Ctxt;
  51715. end;
  51716. end;
  51717. finally
  51718. LogRestBatch.Safe.UnLock;
  51719. end;
  51720. end;
  51721. begin
  51722. if mlInterfaces in TSQLRestServer(Rest).StatLevels then
  51723. QueryPerformanceCounter(timeStart);
  51724. // 1. initialize Inst.Instance and Inst.InstanceID
  51725. Inst.InstanceID := 0;
  51726. Inst.Instance := nil;
  51727. case InstanceCreation of
  51728. sicSingle:
  51729. Inst.Instance := CreateInstance(true);
  51730. sicShared:
  51731. Inst.Instance := fSharedInstance;
  51732. sicClientDriven, sicPerSession, sicPerUser, sicPerGroup, sicPerThread: begin
  51733. case InstanceCreation of
  51734. sicClientDriven:
  51735. Inst.InstanceID := Ctxt.ServiceInstanceID;
  51736. sicPerThread:
  51737. Inst.InstanceID := PtrUInt(GetCurrentThreadId);
  51738. else
  51739. if Ctxt.Session>CONST_AUTHENTICATION_NOT_USED then
  51740. case InstanceCreation of // authenticated user -> handle context
  51741. sicPerSession: Inst.InstanceID := Ctxt.Session;
  51742. sicPerUser: Inst.InstanceID := Ctxt.SessionUser;
  51743. sicPerGroup: Inst.InstanceID := Ctxt.SessionGroup;
  51744. end else begin
  51745. Error('mode expects an authenticated session',HTML_UNAUTHORIZED);
  51746. exit;
  51747. end;
  51748. end;
  51749. if InternalInstanceRetrieve(Inst,Ctxt.ServiceMethodIndex) then begin
  51750. Ctxt.Success; // was SERVICE_METHODINDEX_FREEINSTANCE
  51751. exit; // {"method":"_free_", "params":[], "id":1234}
  51752. end;
  51753. end;
  51754. end;
  51755. if Inst.Instance=nil then begin
  51756. Error('instance not found or deprecated',HTML_BADREQUEST);
  51757. exit;
  51758. end;
  51759. Ctxt.ServiceInstanceID := Inst.InstanceID;
  51760. // 2. call method implementation
  51761. if (Ctxt.ServiceExecution=nil) or
  51762. (cardinal(Ctxt.ServiceMethodIndex)>=fInterface.fMethodsCount) then begin
  51763. Error('ServiceExecution=nil',HTML_SERVERERROR);
  51764. exit;
  51765. end;
  51766. if mlInterfaces in TSQLRestServer(Rest).StatLevels then begin
  51767. stats := fStats[Ctxt.ServiceMethodIndex];
  51768. if stats=nil then begin
  51769. stats := StatsCreate;
  51770. fStats[Ctxt.ServiceMethodIndex] := stats;
  51771. end;
  51772. stats.Processing := true;
  51773. end else
  51774. stats := nil;
  51775. exec := nil;
  51776. try
  51777. if fImplementationClassKind=ickFake then
  51778. if Inst.Instance<>fSharedInstance then
  51779. exit else
  51780. instancePtr := @TInterfacedObjectFake(Inst.Instance).fVTable else begin
  51781. if PClass(Inst.Instance)^=fImplementationClass then
  51782. entry := fImplementationClassInterfaceEntry else begin
  51783. entry := Inst.Instance.GetInterfaceEntry(fInterface.fInterfaceIID);
  51784. if entry=nil then
  51785. exit;
  51786. end;
  51787. instancePtr := PAnsiChar(Inst.Instance)+entry^.IOffset;
  51788. end;
  51789. if optExecInPerInterfaceThread in Ctxt.ServiceExecution.Options then
  51790. if fBackgroundThread=nil then
  51791. fBackgroundThread := Rest.NewBackgroundThreadMethod(
  51792. '% %',[self,fInterface.fInterfaceName]);
  51793. WR := TJSONSerializer.CreateOwnedStream;
  51794. try
  51795. Ctxt.fThreadServer^.Factory := self;
  51796. if (Ctxt.Call.InHead='') or (Ctxt.ClientKind=ckFramework) then
  51797. include(WR.fCustomOptions,twoForceJSONExtended) else
  51798. include(WR.fCustomOptions,twoForceJSONStandard); // AJAX
  51799. // root/calculator {"method":"add","params":[1,2]} -> {"result":[3],"id":0}
  51800. Ctxt.ServiceResultStart(WR);
  51801. dolock := optExecLockedPerInterface in Ctxt.ServiceExecution.Options;
  51802. if dolock then
  51803. EnterCriticalSection(fInstanceLock);
  51804. exec := TServiceMethodExecute.Create(@fInterface.fMethods[Ctxt.ServiceMethodIndex]);
  51805. try
  51806. exec.fOptions := Ctxt.ServiceExecution.Options;
  51807. {$ifndef LVCL}
  51808. exec.fBackgroundExecutionThread := fBackgroundThread;
  51809. {$endif}
  51810. exec.fOnCallback := Ctxt.ExecuteCallback;
  51811. if fOnExecute<>nil then
  51812. MultiEventMerge(exec.fOnExecute,fOnExecute);
  51813. if Ctxt.ServiceExecution.LogRest<>nil then
  51814. exec.AddInterceptor(OnLogRestExecuteMethod);
  51815. if exec.ExecuteJson([instancePtr],Ctxt.ServiceParameters,WR,Ctxt.ForceServiceResultAsJSONObject) then begin
  51816. Ctxt.Call.OutHead := exec.ServiceCustomAnswerHead;
  51817. Ctxt.Call.OutStatus := exec.ServiceCustomAnswerStatus;
  51818. end else begin
  51819. Error('execution failed (probably due to bad input parameters)',HTML_NOTACCEPTABLE);
  51820. exit; // wrong request
  51821. end;
  51822. finally
  51823. if dolock then
  51824. LeaveCriticalSection(fInstanceLock);
  51825. end;
  51826. if Ctxt.Call.OutHead='' then begin // <>'' for TServiceCustomAnswer
  51827. Ctxt.ServiceResultEnd(WR,Inst.InstanceID);
  51828. Ctxt.Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR;
  51829. Ctxt.Call.OutStatus := HTML_SUCCESS;
  51830. end;
  51831. WR.SetText(Ctxt.Call.OutBody);
  51832. finally
  51833. Ctxt.fThreadServer^.Factory := nil;
  51834. WR.Free;
  51835. end;
  51836. finally
  51837. if InstanceCreation=sicSingle then
  51838. Inst.SafeFreeInstance(self); // always release single shot instance
  51839. if stats<>nil then begin
  51840. QueryPerformanceCounter(timeEnd);
  51841. dec(timeEnd,timeStart);
  51842. Ctxt.StatsFromContext(stats,timeEnd,false);
  51843. if Ctxt.Server.StatUsage<>nil then
  51844. Ctxt.Server.StatUsage.Modified(stats,[]);
  51845. if (mlSessions in TSQLRestServer(Rest).StatLevels) and (Ctxt.fAuthSession<>nil) then begin
  51846. if Ctxt.fAuthSession.fInterfaces=nil then
  51847. SetLength(Ctxt.fAuthSession.fInterfaces,length(Rest.Services.fListInterfaceMethod));
  51848. m := Ctxt.fServiceListInterfaceMethodIndex;
  51849. if m<0 then
  51850. m := Rest.Services.fListInterfaceMethods.FindHashed(
  51851. fInterface.fMethods[Ctxt.ServiceMethodIndex].InterfaceDotMethodName);
  51852. if m>=0 then
  51853. with Ctxt.fAuthSession do begin
  51854. stats := fInterfaces[m];
  51855. if stats=nil then begin
  51856. stats := StatsCreate;
  51857. fInterfaces[m] := stats;
  51858. end;
  51859. Ctxt.StatsFromContext(stats,timeEnd,true);
  51860. // mlSessions stats are not yet tracked per Client
  51861. end;
  51862. end;
  51863. end else
  51864. timeEnd := 0;
  51865. if exec<>nil then begin
  51866. if Ctxt.ServiceExecution.LogRest<>nil then
  51867. FinalizeLogRest;
  51868. exec.Free;
  51869. end;
  51870. end;
  51871. end;
  51872. function TServiceFactoryServer.AllowAll: TServiceFactoryServer;
  51873. var m: integer;
  51874. begin
  51875. if self<>nil then
  51876. for m := 0 to fInterface.fMethodsCount-1 do
  51877. FillcharFast(fExecution[m].Denied,sizeof(fExecution[m].Denied),0);
  51878. result := self;
  51879. end;
  51880. function TServiceFactoryServer.AllowAllByID(const aGroupID: array of TID): TServiceFactoryServer;
  51881. var m,g: integer;
  51882. begin
  51883. if self<>nil then
  51884. for m := 0 to fInterface.fMethodsCount-1 do
  51885. with fExecution[m] do
  51886. for g := 0 to high(aGroupID) do
  51887. exclude(Denied,aGroupID[g]-1);
  51888. result := self;
  51889. end;
  51890. function TServiceFactoryServer.AllowAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer;
  51891. var IDs: TIDDynArray;
  51892. begin
  51893. if self<>nil then
  51894. if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then
  51895. AllowAllByID(IDs);
  51896. result := self;
  51897. end;
  51898. function TServiceFactoryServer.DenyAll: TServiceFactoryServer;
  51899. var m: integer;
  51900. begin
  51901. if self<>nil then
  51902. for m := 0 to fInterface.fMethodsCount-1 do
  51903. FillcharFast(fExecution[m].Denied,sizeof(fExecution[m].Denied),255);
  51904. result := self;
  51905. end;
  51906. function TServiceFactoryServer.DenyAllByID(const aGroupID: array of TID): TServiceFactoryServer;
  51907. var m,g: integer;
  51908. begin
  51909. if self<>nil then
  51910. for m := 0 to fInterface.fMethodsCount-1 do
  51911. with fExecution[m] do
  51912. for g := 0 to high(aGroupID) do
  51913. include(Denied,aGroupID[g]-1);
  51914. result := self;
  51915. end;
  51916. function TServiceFactoryServer.DenyAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer;
  51917. var IDs: TIDDynArray;
  51918. begin
  51919. if self<>nil then
  51920. if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then
  51921. DenyAllByID(IDs);
  51922. result := self;
  51923. end;
  51924. function TServiceFactoryServer.Allow(const aMethod: array of RawUTF8): TServiceFactoryServer;
  51925. var m: integer;
  51926. begin
  51927. if self<>nil then
  51928. for m := 0 to high(aMethod) do
  51929. FillcharFast(fExecution[fInterface.CheckMethodIndex(aMethod[m])].Denied,
  51930. sizeof(fExecution[0].Denied),0);
  51931. result := self;
  51932. end;
  51933. function TServiceFactoryServer.AllowByID(const aMethod: array of RawUTF8;
  51934. const aGroupID: array of TID): TServiceFactoryServer;
  51935. var m,g: integer;
  51936. begin
  51937. if self<>nil then
  51938. if high(aGroupID)>=0 then
  51939. for m := 0 to high(aMethod) do
  51940. with fExecution[fInterface.CheckMethodIndex(aMethod[m])] do
  51941. for g := 0 to high(aGroupID) do
  51942. exclude(Denied,aGroupID[g]-1);
  51943. result := self;
  51944. end;
  51945. function TServiceFactoryServer.AllowByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer;
  51946. var IDs: TIDDynArray;
  51947. begin
  51948. if self<>nil then
  51949. if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then
  51950. AllowByID(aMethod,IDs);
  51951. result := self;
  51952. end;
  51953. function TServiceFactoryServer.Deny(const aMethod: array of RawUTF8): TServiceFactoryServer;
  51954. var m: integer;
  51955. begin
  51956. if self<>nil then
  51957. for m := 0 to high(aMethod) do
  51958. FillcharFast(fExecution[fInterface.CheckMethodIndex(aMethod[m])].Denied,
  51959. sizeof(fExecution[0].Denied),255);
  51960. result := self;
  51961. end;
  51962. function TServiceFactoryServer.DenyByID(const aMethod: array of RawUTF8;
  51963. const aGroupID: array of TID): TServiceFactoryServer;
  51964. var m,g: integer;
  51965. begin
  51966. if self<>nil then
  51967. for m := 0 to high(aMethod) do
  51968. with fExecution[fInterface.CheckMethodIndex(aMethod[m])] do
  51969. for g := 0 to high(aGroupID) do
  51970. include(Denied,aGroupID[g]-1);
  51971. result := self;
  51972. end;
  51973. function TServiceFactoryServer.DenyByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer;
  51974. var IDs: TIDDynArray;
  51975. begin
  51976. if self<>nil then
  51977. if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then
  51978. DenyByID(aMethod,IDs);
  51979. result := self;
  51980. end;
  51981. function TServiceFactoryServer.SetOptions(const aMethod: array of RawUTF8;
  51982. aOptions: TServiceMethodOptions): TServiceFactoryServer;
  51983. var m,i: integer;
  51984. begin
  51985. if self<>nil then begin
  51986. if (fInstanceCreation=sicPerThread) and (optExecLockedPerInterface in aOptions) then
  51987. raise EServiceException.CreateUTF8('%.SetOptions(I%,optExecLockedPerInterface)'+
  51988. ' not compatible with sicPerThread',[self,fInterfaceURI]);
  51989. if (fInstanceCreation=sicPerThread) and
  51990. ([{$ifndef LVCL}optExecInMainThread,optFreeInMainThread,{$endif}
  51991. optExecInPerInterfaceThread,optFreeInPerInterfaceThread]*aOptions<>[]) then
  51992. raise EServiceException.CreateUTF8('%.SetOptions(I%,opt*In*Thread) '+
  51993. 'not compatible with sicPerThread',[self,fInterfaceURI]);
  51994. {$ifndef LVCL}
  51995. if (optExecLockedPerInterface in aOptions) and
  51996. ([optExecInMainThread,optFreeInMainThread,
  51997. optExecInPerInterfaceThread,optFreeInPerInterfaceThread]*aOptions<>[]) then
  51998. raise EServiceException.CreateUTF8('%.SetOptions(I%,optExecLockedPerInterface)'+
  51999. ' with opt*In*Thread options',[self,fInterfaceURI]);
  52000. {$endif}
  52001. if high(aMethod)<0 then
  52002. for i := 0 to fInterface.fMethodsCount-1 do
  52003. fExecution[i].Options := aOptions else
  52004. for m := 0 to high(aMethod) do
  52005. fExecution[fInterface.CheckMethodIndex(aMethod[m])].Options := aOptions;
  52006. fAnyOptions := [];
  52007. for i := 0 to fInterface.fMethodsCount-1 do
  52008. fAnyOptions := fAnyOptions+fExecution[i].Options;
  52009. if (optFreeInPerInterfaceThread in fAnyOptions) and
  52010. not (optExecInPerInterfaceThread in fAnyOptions) then
  52011. raise EServiceException.CreateUTF8('%.SetOptions(I%,optFreeInPerInterfaceThread)'+
  52012. ' without optExecInPerInterfaceThread',[self,fInterfaceURI]);
  52013. {$ifndef LVCL}
  52014. if ([optExecInMainThread,optFreeInMainThread]*fAnyOptions<>[]) and
  52015. ([optExecInPerInterfaceThread,optFreeInPerInterfaceThread]*fAnyOptions<>[]) then
  52016. raise EServiceException.CreateUTF8('%.SetOptions(I%): concurrent '+
  52017. 'opt*InMainThread and opt*InPerInterfaceThread',[self,fInterfaceURI]);
  52018. {$endif}
  52019. end;
  52020. result := self;
  52021. end;
  52022. function TServiceFactoryServer.SetTimeoutSec(value: cardinal): TServiceFactoryServer;
  52023. begin
  52024. SetTimeoutSecInt(value);
  52025. result := self;
  52026. end;
  52027. function TServiceFactoryServer.SetServiceLog(const aMethod: array of RawUTF8;
  52028. aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass): TServiceFactoryServer;
  52029. var bits: TInterfaceFactoryMethodBits;
  52030. begin
  52031. if self<>nil then begin
  52032. fInterface.CheckMethodIndexes(aMethod,true,bits);
  52033. SetServiceLogByIndex(bits,aLogRest,aLogClass);
  52034. end;
  52035. result := self;
  52036. end;
  52037. procedure TServiceFactoryServer.SetServiceLogByIndex(
  52038. const aMethods: TInterfaceFactoryMethodBits; aLogRest: TSQLRest;
  52039. aLogClass: TSQLRecordServiceLogClass);
  52040. procedure SetEntry(i,ndx: integer);
  52041. var j: integer;
  52042. begin
  52043. with fExecution[i] do begin
  52044. if LogRestBatch.Count>0 then begin
  52045. LogRest.BatchSend(LogRestBatch);
  52046. LogRestBatch.Reset;
  52047. end;
  52048. LogRest := aLogRest;
  52049. LogClassModelIndex := ndx;
  52050. if LogRest=nil then
  52051. exit;
  52052. for j := 0 to High(fLogRestBatch) do
  52053. if fLogRestBatch[j].Rest=LogRest then begin
  52054. LogRestBatch := fLogRestBatch[j];
  52055. exit; // already assigned to the very same TSQLRest instance
  52056. end;
  52057. LogRestBatch := TSQLRestBatchLocked.Create(LogRest,
  52058. LogRest.Model.Tables[ndx],10000);
  52059. ObjArrayAdd(fLogRestBatch,LogRestBatch);
  52060. end;
  52061. end;
  52062. var i,ndx: integer;
  52063. begin
  52064. if aLogRest=nil then
  52065. ndx := -1 else
  52066. with aLogRest.Model do
  52067. if aLogClass=nil then begin
  52068. ndx := GetTableIndexInheritsFrom(TSQLRecordServiceLog);
  52069. if ndx<0 then
  52070. raise EModelException.CreateUTF8('%.SetServiceLog: Missing '+
  52071. 'TSQLRecordServiceLog class in %.Model',[self,aLogRest]);
  52072. end else
  52073. ndx := GetTableIndexExisting(aLogClass);
  52074. for i := 0 to fInterface.fMethodsCount-1 do
  52075. if i in aMethods then
  52076. SetEntry(i,ndx);
  52077. end;
  52078. procedure TServiceFactoryServer.AddInterceptor(const Hook: TServiceMethodExecuteEvent);
  52079. begin
  52080. MultiEventAdd(fOnExecute,TMethod(Hook));
  52081. end;
  52082. { TServiceRecordVersion }
  52083. function TServiceRecordVersion.Subscribe(const SQLTableName: RawUTF8;
  52084. const revision: TRecordVersion; const callback: IServiceRecordVersionCallback): boolean;
  52085. var tableIndex: integer;
  52086. tableRemote: TSQLRest;
  52087. tableServer: TSQLRestServer;
  52088. begin
  52089. if Server<>nil then begin
  52090. tableIndex := Server.Model.GetTableIndex(SQLTableName);
  52091. if tableIndex>=0 then begin
  52092. tableRemote := Server.GetRemoteTable(tableIndex);
  52093. if (tableRemote=nil) or not tableRemote.InheritsFrom(TSQLRestServer) then
  52094. tableServer := Server else
  52095. tableServer := TSQLRestServer(tableRemote);
  52096. result := tableServer.RecordVersionSynchronizeSubscribeMaster(
  52097. Server.Model.Tables[tableindex],revision,callback);
  52098. exit;
  52099. end;
  52100. end;
  52101. result := false;
  52102. end;
  52103. { TServiceMethodArgument }
  52104. {$ifndef FPC}
  52105. procedure TServiceMethodArgument.SetFromRTTI(var P: PByte);
  52106. var PS: PShortString absolute P;
  52107. PP: ^PPTypeInfo absolute P;
  52108. begin
  52109. ArgTypeName := PS;
  52110. PS := AlignToPtr(@PS^[ord(PS^[0])+1]);
  52111. if PP^=nil then
  52112. {$ifndef ISDELPHI2010}
  52113. if IdemPropName(ArgTypeName^,'TGUID') then
  52114. ArgTypeInfo := @GUID_FAKETYPEINFO else
  52115. {$endif}
  52116. raise EInterfaceFactoryException.CreateUTF8(
  52117. '"%: %" parameter has no RTTI',[ParamName^,ArgTypeName^]) else
  52118. ArgTypeInfo := PP^^;
  52119. inc(PP);
  52120. end;
  52121. {$endif FPC}
  52122. procedure TServiceMethodArgument.SerializeToContract(WR: TTextWriter);
  52123. const
  52124. CONST_ARGDIRTOJSON: array[TServiceMethodValueDirection] of string[4] = (
  52125. // convert into generic in/out direction (assume result is out)
  52126. 'in','both','out','out');
  52127. // AnsiString (Delphi <2009) may loose data depending on the client
  52128. CONST_ARGTYPETOJSON: array[TServiceMethodValueType] of string[8] = (
  52129. '??','self','boolean', '', '','integer','cardinal','int64',
  52130. 'double','datetime','currency','utf8','utf8','utf8','utf8','',
  52131. {$ifndef NOVARIANTS}'variant',{$endif}'','json','','');
  52132. begin
  52133. WR.AddShort('{"argument":"');
  52134. WR.AddShort(ParamName^);
  52135. WR.AddShort('","direction":"');
  52136. WR.AddShort(CONST_ARGDIRTOJSON[ValueDirection]);
  52137. WR.AddShort('","type":"');
  52138. if CONST_ARGTYPETOJSON[ValueType]='' then
  52139. WR.AddShort(ArgTypeInfo^.Name) else
  52140. WR.AddShort(CONST_ARGTYPETOJSON[ValueType]);
  52141. WR.AddShort('"},');
  52142. end;
  52143. procedure TServiceMethodArgument.AddJSON(WR: TTextWriter; V: pointer);
  52144. begin
  52145. if vIsString in ValueKindAsm then
  52146. WR.Add('"');
  52147. case ValueType of
  52148. smvBoolean: WR.Add(PBoolean(V)^);
  52149. smvEnum..smvInt64:
  52150. case SizeInStorage of
  52151. 1: WR.Add(PByte(V)^);
  52152. 2: WR.Add(PWord(V)^);
  52153. 4: if ValueType=smvInteger then
  52154. WR.Add(PInteger(V)^) else
  52155. WR.AddU(PCardinal(V)^);
  52156. 8: WR.Add(PInt64(V)^);
  52157. end;
  52158. smvDouble, smvDateTime: WR.AddDouble(PDouble(V)^);
  52159. smvCurrency: WR.AddCurr64(PInt64(V)^);
  52160. smvRawUTF8: WR.AddJSONEscape(PPointer(V)^);
  52161. smvRawJSON: WR.AddNoJSONEscape(PPointer(V)^,length(PRawUTF8(V)^));
  52162. smvString: {$ifdef UNICODE}
  52163. WR.AddJSONEscapeW(pointer(PString(V)^));
  52164. {$else}
  52165. WR.AddJSONEscapeAnsiString(PString(V)^);
  52166. {$endif}
  52167. smvRawByteString: WR.WrBase64(PPointer(V)^,length(PRawBytestring(V)^),false);
  52168. smvWideString: WR.AddJSONEscapeW(PPointer(V)^);
  52169. smvObject: WR.WriteObject(PPointer(V)^);
  52170. smvInterface: WR.AddShort('null'); // or written by InterfaceWrite()
  52171. smvRecord: WR.AddRecordJSON(V^,ArgTypeInfo);
  52172. smvDynArray: WR.AddDynArrayJSON(ArgTypeInfo,V^);
  52173. {$ifndef NOVARIANTS}
  52174. smvVariant: WR.AddVariant(PVariant(V)^,twJSONEscape);
  52175. {$endif}
  52176. end;
  52177. if vIsString in ValueKindAsm then
  52178. WR.Add('"',',') else
  52179. WR.Add(',');
  52180. end;
  52181. procedure TServiceMethodArgument.AsJson(var DestValue: RawUTF8; V: pointer);
  52182. var W: TTextWriter;
  52183. begin
  52184. case ValueType of // some direct conversion of simple types
  52185. smvBoolean:
  52186. JSONBoolean(PBoolean(V)^,DestValue);
  52187. smvEnum..smvInt64:
  52188. case SizeInStorage of
  52189. 1: UInt32ToUtf8(PByte(V)^,DestValue);
  52190. 2: UInt32ToUtf8(PWord(V)^,DestValue);
  52191. 4: if ValueType=smvInteger then
  52192. Int32ToUtf8(PInteger(V)^,DestValue) else
  52193. UInt32ToUtf8(PCardinal(V)^,DestValue);
  52194. 8: Int64ToUtf8(PInt64(V)^,DestValue);
  52195. end;
  52196. smvDouble, smvDateTime:
  52197. ExtendedToStr(PDouble(V)^,DOUBLE_PRECISION,DestValue);
  52198. smvCurrency:
  52199. Curr64ToStr(PInt64(V)^,DestValue);
  52200. smvRawJSON:
  52201. DestValue := PRawUTF8(V)^;
  52202. else begin // use generic AddJSON() method
  52203. W := TJSONSerializer.CreateOwnedStream(512);
  52204. try
  52205. AddJSON(W,V);
  52206. W.SetText(DestValue);
  52207. finally
  52208. W.Free;
  52209. end;
  52210. end;
  52211. end;
  52212. end;
  52213. procedure TServiceMethodArgument.AddJSONEscaped(WR: TTextWriter; V: pointer);
  52214. var W: TTextWriter;
  52215. begin
  52216. if ValueType in [smvBoolean..smvCurrency,smvInterface] then // no need to escape those
  52217. AddJSON(WR,V) else begin
  52218. W := WR.InternalJSONWriter;
  52219. AddJSON(W,V);
  52220. WR.AddJSONEscape(W);
  52221. end;
  52222. end;
  52223. procedure TServiceMethodArgument.AddValueJSON(WR: TTextWriter; const Value: RawUTF8);
  52224. begin
  52225. if vIsString in ValueKindAsm then begin
  52226. WR.Add('"');
  52227. WR.AddJSONEscape(pointer(Value),length(Value));
  52228. WR.Add('"',',');
  52229. end else begin
  52230. WR.AddString(Value);
  52231. WR.Add(',');
  52232. end;
  52233. end;
  52234. procedure TServiceMethodArgument.AddDefaultJSON(WR: TTextWriter);
  52235. begin
  52236. case ValueType of
  52237. smvBoolean: WR.AddShort('false,');
  52238. smvObject: WR.AddShort('null,'); // may raise an error on the client side
  52239. smvInterface: WR.AddShort('0,');
  52240. smvDynArray: WR.AddShort('[],');
  52241. smvRecord: begin
  52242. WR.AddVoidRecordJSON(ArgTypeInfo);
  52243. WR.Add(',');
  52244. end;
  52245. {$ifndef NOVARIANTS}
  52246. smvVariant: WR.AddShort('null,');
  52247. {$endif}
  52248. else
  52249. if vIsString in ValueKindAsm then
  52250. WR.AddShort('"",') else
  52251. WR.AddShort('0,');
  52252. end;
  52253. end;
  52254. {$ifndef NOVARIANTS}
  52255. procedure TServiceMethodArgument.AsVariant(var DestValue: variant; V: pointer;
  52256. Options: TDocVariantOptions);
  52257. var tmp: RawUTF8;
  52258. begin
  52259. case ValueType of // some direct conversion of simple types
  52260. smvBoolean:
  52261. DestValue := PBoolean(V)^;
  52262. smvEnum..smvInt64:
  52263. case SizeInStorage of
  52264. 1: DestValue := PByte(V)^;
  52265. 2: DestValue := PWord(V)^;
  52266. 4: if ValueType=smvInteger then
  52267. DestValue := PInteger(V)^ else
  52268. DestValue := PCardinal(V)^;
  52269. 8: DestValue := PInt64(V)^;
  52270. end;
  52271. smvDouble, smvDateTime:
  52272. DestValue := PDouble(V)^;
  52273. smvCurrency:
  52274. DestValue := PCurrency(V)^;
  52275. smvRawUTF8:
  52276. RawUTF8ToVariant(PRawUTF8(V)^,DestValue);
  52277. smvString: begin
  52278. StringToUTF8(PString(V)^,tmp);
  52279. RawUTF8ToVariant(tmp,DestValue);
  52280. end;
  52281. smvWideString: begin
  52282. RawUnicodeToUtf8(PPointer(V)^,length(PWideString(V)^),tmp);
  52283. RawUTF8ToVariant(tmp,DestValue);
  52284. end;
  52285. smvVariant:
  52286. DestValue := PVariant(V)^;
  52287. else begin // use generic AddJSON() method
  52288. AsJson(tmp,V);
  52289. VariantLoadJSON(DestValue,pointer(tmp),nil,@Options);
  52290. end;
  52291. end;
  52292. end;
  52293. procedure TServiceMethodArgument.AddAsVariant(var Dest: TDocVariantData; V: pointer);
  52294. var tmp: variant;
  52295. begin
  52296. AsVariant(tmp,V,Dest.Options);
  52297. if Dest.Kind=dvArray then
  52298. Dest.AddItem(tmp) else
  52299. Dest.AddValue(ShortStringToAnsi7String(ParamName^),tmp);
  52300. end;
  52301. procedure TServiceMethodArgument.FixValueAndAddToObject(const Value: variant;
  52302. var DestDoc: TDocVariantData);
  52303. var tempCopy: variant;
  52304. begin
  52305. tempCopy := Value;
  52306. FixValue(tempCopy);
  52307. DestDoc.AddValue(ShortStringToAnsi7String(ParamName^),tempCopy);
  52308. end;
  52309. procedure TServiceMethodArgument.FixValue(var Value: variant);
  52310. var enum: Int64;
  52311. obj: TObject;
  52312. arr: pointer;
  52313. dyn: TDynArray;
  52314. rec: TByteDynArray;
  52315. json: RawUTF8;
  52316. begin
  52317. case ValueType of
  52318. smvEnum:
  52319. if VariantToInt64(Value,enum) then
  52320. Value := PTypeInfo(ArgTypeInfo)^.EnumBaseType^.GetEnumNameOrd(enum)^;
  52321. smvSet:
  52322. if VariantToInt64(Value,enum) then
  52323. Value := PTypeInfo(ArgTypeInfo)^.SetEnumType^.GetSetNameAsDocVariant(enum);
  52324. smvObject: begin
  52325. obj := ArgTypeInfo^.ClassCreate;
  52326. try
  52327. if DocVariantToObject(_Safe(Value)^,obj) then
  52328. Value := _ObjFast(obj,[woEnumSetsAsText]);
  52329. finally
  52330. obj.Free;
  52331. end;
  52332. end;
  52333. smvDynArray:
  52334. if _Safe(Value)^.Kind=dvArray then begin
  52335. arr := nil;
  52336. dyn.Init(ArgTypeInfo,arr);
  52337. try
  52338. VariantSaveJSON(Value,twJSONEscape,json);
  52339. dyn.LoadFromJSON(pointer(json));
  52340. json := dyn.SaveToJSON(true);
  52341. Value := _JsonFast(json);
  52342. finally
  52343. dyn.Clear;
  52344. end;
  52345. end;
  52346. smvRecord:
  52347. if _Safe(Value)^.Kind=dvObject then begin
  52348. SetLength(rec,ArgTypeInfo^.RecordType^.Size);
  52349. try
  52350. VariantSaveJSON(Value,twJSONEscape,json);
  52351. RecordLoadJSON(rec[0],pointer(json),ArgTypeInfo);
  52352. json := RecordSaveJSON(rec[0],ArgTypeInfo,true);
  52353. Value := _JsonFast(json);
  52354. finally
  52355. RecordClear(rec[0],ArgTypeInfo);
  52356. end;
  52357. end;
  52358. end;
  52359. end;
  52360. {$endif NOVARIANTS}
  52361. { TAutoCreateFields }
  52362. type // use AutoTable VMT entry to store a cache of the needed fields RTTI
  52363. TAutoCreateFields = class
  52364. public
  52365. ClassesCount: integer;
  52366. ObjArraysCount: integer;
  52367. Classes: array of record
  52368. Offset: cardinal;
  52369. Instance: TClassInstance;
  52370. end;
  52371. ObjArraysOffset: array of cardinal;
  52372. constructor Create(aClass: TClass);
  52373. end;
  52374. constructor TAutoCreateFields.Create(aClass: TClass);
  52375. var i: integer;
  52376. P: PPropInfo;
  52377. begin
  52378. repeat
  52379. for i := 1 to InternalClassPropInfo(aClass,P) do begin
  52380. case P^.PropType^.Kind of
  52381. tkClass: begin
  52382. if (P^.SetProc<>0) or not P^.GetterIsField then
  52383. raise EModelException.CreateUTF8('%.%: % is an auto-created instance '+
  52384. 'so should not have any "write" defined',[aClass,P^.Name,P^.PropType^.Name]);
  52385. SetLength(Classes,ClassesCount+1);
  52386. with Classes[ClassesCount] do begin
  52387. Offset := PtrUInt(P^.GetterAddr(nil));
  52388. Instance.Init(P^.PropType^.ClassType^.ClassType);
  52389. end;
  52390. inc(ClassesCount);
  52391. end;
  52392. tkDynArray:
  52393. if (ObjArraySerializers.Find(P^.TypeInfo)<>nil)
  52394. and P^.GetterIsField then begin
  52395. SetLength(ObjArraysOffset,ObjArraysCount+1);
  52396. ObjArraysOffset[ObjArraysCount] := PtrUInt(P^.GetterAddr(nil));
  52397. inc(ObjArraysCount);
  52398. end;
  52399. end;
  52400. P := P^.Next;
  52401. end;
  52402. aClass := aClass.ClassParent;
  52403. until aClass=TObject;
  52404. end;
  52405. type
  52406. TSimpleMethodCall = procedure(self: TObject);
  52407. procedure AutoCreateFields(self: TObject);
  52408. var fields: TAutoCreateFields;
  52409. PVMT: PPointer;
  52410. i: integer;
  52411. begin
  52412. PVMT := pointer(PPtrInt(self)^+vmtAutoTable);
  52413. fields := PVMT^;
  52414. if fields=nil then begin
  52415. // first time access: compute RTTI cache
  52416. fields := TAutoCreateFields.Create(PClass(self)^);
  52417. // store the RTTI cache into the AutoTable VMT entry of this class
  52418. PatchCodePtrUInt(pointer(PVMT),PtrUInt(fields),true);
  52419. GarbageCollectorFreeAndNil(PVMT^,fields);
  52420. end else
  52421. if PClass(fields)^<>TAutoCreateFields then
  52422. raise EModelException.CreateUTF8('%.AutoTable VMT entry already set',[self]);
  52423. // auto-create published persistent class instances
  52424. for i := 0 to fields.ClassesCount-1 do
  52425. with fields.Classes[i] do
  52426. PObject(PtrUInt(self)+Offset)^ := Instance.CreateNew;
  52427. end;
  52428. procedure AutoDestroyFields(self: TObject);
  52429. {$ifdef HASINLINE}inline;{$endif}
  52430. var i: integer;
  52431. fields: TAutoCreateFields;
  52432. begin
  52433. fields := PPointer(PPtrInt(self)^+vmtAutoTable)^;
  52434. if fields=nil then
  52435. exit; // may happen in a weird finalization code
  52436. // auto-release published persistent class instances
  52437. for i := 0 to fields.ClassesCount-1 do
  52438. PObject(PtrUInt(self)+fields.Classes[i].Offset)^.Free;
  52439. // auto-release published T*ObjArray instances
  52440. for i := 0 to fields.ObjArraysCount-1 do
  52441. ObjArrayClear(pointer(PtrUInt(self)+fields.ObjArraysOffset[i])^);
  52442. end;
  52443. { TPersistentAutoCreateFields }
  52444. constructor TPersistentAutoCreateFields.Create;
  52445. begin
  52446. AutoCreateFields(self);
  52447. inherited Create; // always call the virtual constructor
  52448. end;
  52449. destructor TPersistentAutoCreateFields.Destroy;
  52450. begin
  52451. AutoDestroyFields(self);
  52452. inherited Destroy;
  52453. end;
  52454. { TSynAutoCreateFields }
  52455. {$ifdef FPC_OR_PUREPASCAL}
  52456. constructor TSynAutoCreateFields.Create;
  52457. begin
  52458. AutoCreateFields(self);
  52459. inherited Create; // always call the virtual constructor
  52460. end;
  52461. {$else}
  52462. class function TSynAutoCreateFields.NewInstance: TObject;
  52463. asm
  52464. push eax // class
  52465. mov eax,[eax].vmtInstanceSize
  52466. push eax // size
  52467. call System.@GetMem
  52468. pop edx // size
  52469. push eax // self
  52470. mov cl,0
  52471. call dword ptr [FillcharFast]
  52472. pop eax // self
  52473. pop edx // class
  52474. mov [eax],edx // store VMT
  52475. push eax
  52476. call AutoCreateFields
  52477. pop eax
  52478. end; // ignore vmtIntfTable for this class hierarchy (won't implement interfaces)
  52479. {$endif}
  52480. destructor TSynAutoCreateFields.Destroy;
  52481. begin
  52482. AutoDestroyFields(self);
  52483. inherited Destroy;
  52484. end;
  52485. { TSynAutoCreateFieldsLocked }
  52486. constructor TSynAutoCreateFieldsLocked.Create;
  52487. begin
  52488. inherited Create;
  52489. fSafe.Init;
  52490. end;
  52491. destructor TSynAutoCreateFieldsLocked.Destroy;
  52492. begin
  52493. inherited Destroy;
  52494. fSafe.Done;
  52495. end;
  52496. procedure TSynAutoCreateFieldsLocked.Lock;
  52497. begin
  52498. if self<>nil then
  52499. fSafe.Lock;
  52500. end;
  52501. procedure TSynAutoCreateFieldsLocked.UnLock;
  52502. begin
  52503. if self<>nil then
  52504. fSafe.UnLock;
  52505. end;
  52506. { TInterfacedObjectAutoCreateFields }
  52507. constructor TInterfacedObjectAutoCreateFields.Create;
  52508. begin
  52509. AutoCreateFields(self);
  52510. inherited Create; // always call the virtual constructor
  52511. end;
  52512. destructor TInterfacedObjectAutoCreateFields.Destroy;
  52513. begin
  52514. AutoDestroyFields(self);
  52515. inherited Destroy;
  52516. end;
  52517. { TInjectableAutoCreateFields }
  52518. constructor TInjectableAutoCreateFields.Create;
  52519. var Inject: IAutoCreateFieldsResolve;
  52520. begin
  52521. AutoCreateFields(self);
  52522. inherited Create; // overriden method will inject its dependencies (DI/IoC)
  52523. if TryResolve(TypeInfo(IAutoCreateFieldsResolve),Inject) then
  52524. Inject.SetProperties(self);
  52525. end;
  52526. destructor TInjectableAutoCreateFields.Destroy;
  52527. begin
  52528. AutoDestroyFields(self);
  52529. inherited Destroy;
  52530. end;
  52531. {$ifndef LVCL}
  52532. { TInterfacedCollection }
  52533. constructor TInterfacedCollection.Create;
  52534. begin
  52535. inherited Create(GetClass);
  52536. end;
  52537. { TCollectionItemAutoCreateFields }
  52538. constructor TCollectionItemAutoCreateFields.Create(Collection: TCollection);
  52539. begin
  52540. AutoCreateFields(self);
  52541. inherited Create(Collection);
  52542. end;
  52543. destructor TCollectionItemAutoCreateFields.Destroy;
  52544. begin
  52545. AutoDestroyFields(self);
  52546. inherited Destroy;
  52547. end;
  52548. {$endif LVCL}
  52549. { TRawUTF8ObjectCacheSettings }
  52550. constructor TRawUTF8ObjectCacheSettings.Create;
  52551. begin
  52552. inherited Create;
  52553. // release after 2 minutes of inactivity by default
  52554. fTimeOutMS := 2 * 60 * 1000;
  52555. // 1 second periodicity of purge is small enough to be painless
  52556. fPurgePeriodMS := 1000;
  52557. end;
  52558. { TRawUTF8ObjectCache }
  52559. constructor TRawUTF8ObjectCache.Create(aOwner: TRawUTF8ObjectCacheList;
  52560. const aKey: RawUTF8);
  52561. begin
  52562. inherited Create;
  52563. fOwner := aOwner;
  52564. fKey := aKey;
  52565. fOwner.Log('%.Create(%)', [ClassType, fKey]);
  52566. fTimeoutMS := fOwner.fSettings.TimeOutMS;
  52567. end;
  52568. destructor TRawUTF8ObjectCache.Destroy;
  52569. begin
  52570. fOwner.Log('%.Destroy %', [ClassType, fKey]);
  52571. CacheClear;
  52572. inherited Destroy;
  52573. end;
  52574. procedure TRawUTF8ObjectCache.CacheSet;
  52575. begin // gives some addition TTL time
  52576. fTimeoutTix := GetTickCount64 + fTimeoutMS;
  52577. end;
  52578. procedure TRawUTF8ObjectCache.CacheClear;
  52579. begin
  52580. fTimeoutTix := 0; // indicates no service is available
  52581. end;
  52582. function TRawUTF8ObjectCache.Resolve(const aInterface: TGUID; out Obj): boolean;
  52583. begin
  52584. if Assigned(fOwner.OnKeyResolve) then
  52585. result := fOwner.OnKeyResolve(aInterface,fKey,Obj) else
  52586. result := false;
  52587. end;
  52588. { TRawUTF8ObjectCacheList }
  52589. constructor TRawUTF8ObjectCacheList.Create(aClass: TRawUTF8ObjectCacheClass;
  52590. aSettings: TRawUTF8ObjectCacheSettings; aLog: TSynLogFamily; aLogEvent: TSynLogInfo;
  52591. const aOnKeyResolve: TOnKeyResolve);
  52592. begin
  52593. inherited Create(true);
  52594. fClass := aClass;
  52595. fSettings := aSettings;
  52596. if (fClass = nil) or (fClass = TRawUTF8ObjectCache) or (fSettings = nil) then
  52597. raise ESynException.CreateUTF8('%.Create(nil)', [self]);
  52598. if (fSettings.PurgePeriodMS > 0) and (fSettings.TimeOutMS > 0) then
  52599. fNextPurgeTix := GetTickCount64 + fSettings.PurgePeriodMS;
  52600. fLog := aLog;
  52601. fLogEvent := aLogEvent;
  52602. fOnKeyResolve := aOnKeyResolve;
  52603. end;
  52604. procedure TRawUTF8ObjectCacheList.Log(const TextFmt: RawUTF8; const TextArgs: array of const;
  52605. Level: TSynLogInfo);
  52606. begin
  52607. if (self=nil) or (fLog=nil) then
  52608. exit;
  52609. if Level=sllNone then
  52610. Level := fLogEvent;
  52611. fLog.SynLog.Log(Level, TextFmt, TextArgs, self);
  52612. end;
  52613. function TRawUTF8ObjectCacheList.NewObjectCache(const Key: RawUTF8): TRawUTF8ObjectCache;
  52614. begin
  52615. result := fClass.Create(self, Key);
  52616. end;
  52617. procedure TRawUTF8ObjectCacheList.TryPurge;
  52618. begin
  52619. fSafe.Lock;
  52620. try
  52621. if (fNextPurgeTix <> 0) and (GetTickCount64 > fNextPurgeTix) then
  52622. DoPurge;
  52623. finally
  52624. fSafe.UnLock;
  52625. end;
  52626. end;
  52627. procedure TRawUTF8ObjectCacheList.ForceCacheClear;
  52628. var i: integer;
  52629. cache: TRawUTF8ObjectCache;
  52630. begin
  52631. fSafe.Lock;
  52632. try
  52633. fLog.SynLog.Enter('ForceCacheClear of % entries',[fCount],self);
  52634. for i := 0 to fCount - 1 do begin
  52635. cache := TRawUTF8ObjectCache(fObjects[i]);
  52636. cache.fSafe.Lock;
  52637. try
  52638. cache.CacheClear;
  52639. finally
  52640. cache.fSafe.UnLock;
  52641. end;
  52642. end;
  52643. finally
  52644. fSafe.UnLock;
  52645. end;
  52646. end;
  52647. procedure TRawUTF8ObjectCacheList.DoPurge;
  52648. var tix: Int64;
  52649. i: integer;
  52650. purged: RawUTF8;
  52651. log: ISynLog;
  52652. cache: TRawUTF8ObjectCache;
  52653. begin // called within fSafe.Lock
  52654. tix := GetTickCount64;
  52655. try
  52656. for i := 0 to fCount - 1 do begin
  52657. cache := TRawUTF8ObjectCache(fObjects[i]);
  52658. if (cache.fTimeoutTix > 0) and (tix > cache.fTimeoutTix) then
  52659. try // test again the timeout after acquiring the TRawUTF8ObjectCache lock
  52660. cache.Safe.Lock;
  52661. if (cache.fTimeoutTix > 0) and (tix > cache.fTimeoutTix) then begin
  52662. if log = nil then
  52663. log := fLog.SynLog.Enter(self);
  52664. cache.CacheClear; // would set fTimeoutTix := 0
  52665. purged := purged + ' ' + cache.fKey;
  52666. end;
  52667. finally
  52668. cache.Safe.UnLock;
  52669. end;
  52670. end;
  52671. if log <> nil then
  52672. log.Log(fLogEvent, '%.ReleaseServices:% - count=%', [fClass, purged, fCount], self);
  52673. finally
  52674. fNextPurgeTix := tix + fSettings.PurgePeriodMS;
  52675. end;
  52676. end;
  52677. function TRawUTF8ObjectCacheList.GetLocked(const Key: RawUTF8;
  52678. out cache: TRawUTF8ObjectCache; onlyexisting: boolean): boolean;
  52679. var
  52680. added: boolean;
  52681. begin
  52682. result := false;
  52683. if Key = '' then
  52684. exit;
  52685. fSafe.Lock;
  52686. try
  52687. if (fNextPurgeTix <> 0) and (GetTickCount64 > fNextPurgeTix) then
  52688. DoPurge; // inline TryPurge within the locked list
  52689. cache := TRawUTF8ObjectCache(GetObjectByName(Key));
  52690. if cache = nil then begin
  52691. if onlyexisting then begin
  52692. Log('GetLocked(%): onlyexisting=true -> no new %', [Key, fClass]);
  52693. exit;
  52694. end;
  52695. cache := NewObjectCache(Key);
  52696. if cache = nil then begin
  52697. Log('GetLocked: Invalid key - NewObjectCache(%) returned no %', [Key, fClass]);
  52698. exit;
  52699. end;
  52700. AddObjectIfNotExisting(Key, cache, @added);
  52701. if added then
  52702. Log('GetLocked: Added %[%] - count=%', [fClass, Key, fCount])
  52703. else
  52704. raise ESynException.CreateUTF8('%.GetLocked(%) new %', [self, Key, cache]);
  52705. end
  52706. else if cache.fTimeOutTix = 0 then
  52707. Log('GetLocked: Using blank %[%]', [fClass, Key])
  52708. else
  52709. Log('GetLocked: Using %[%] with timeout in % ms',
  52710. [fClass, Key, cache.fTimeOutTix - GetTickCount64]);
  52711. cache.fSafe.Lock;
  52712. result := true;
  52713. finally
  52714. fSafe.UnLock;
  52715. end;
  52716. end;
  52717. { TServiceMethod }
  52718. type
  52719. TDynArrayFake = record
  52720. Value: Pointer;
  52721. Wrapper: TDynArray;
  52722. end;
  52723. function TServiceMethod.ArgIndex(ArgName: PUTF8Char; ArgNameLen: integer;
  52724. Input: boolean): integer;
  52725. begin
  52726. if ArgNameLen>0 then
  52727. if Input then begin
  52728. for result := ArgsInFirst to ArgsInLast do
  52729. with Args[result] do
  52730. if IdemPropName(ParamName^,ArgName,ArgNameLen) then
  52731. if ValueDirection in [smdConst,smdVar] then
  52732. exit else // found
  52733. break; // right name, but wrong direction
  52734. end else
  52735. for result := ArgsOutFirst to ArgsOutLast do
  52736. with Args[result] do
  52737. if IdemPropName(ParamName^,ArgName,ArgNameLen) then
  52738. if ValueDirection in [smdVar,smdOut,smdResult] then
  52739. exit else // found
  52740. break; // right name, but wrong direction
  52741. result := -1;
  52742. end;
  52743. function TServiceMethod.ArgNext(var arg: integer; Input: boolean): boolean;
  52744. begin
  52745. result := true;
  52746. inc(arg);
  52747. if Input then
  52748. while arg<=ArgsInLast do
  52749. if Args[arg].ValueDirection in [smdConst,smdVar] then
  52750. exit else
  52751. inc(arg) else
  52752. while arg<=ArgsOutLast do
  52753. if Args[arg].ValueDirection in [smdVar,smdOut,smdResult] then
  52754. exit else
  52755. inc(arg);
  52756. result := false;
  52757. end;
  52758. function TServiceMethod.ArgsArrayToObject(P: PUTF8Char; Input: boolean): RawUTF8;
  52759. var i: integer;
  52760. W: TTextWriter;
  52761. Value: PUTF8Char;
  52762. begin
  52763. W := TTextWriter.CreateOwnedStream;
  52764. try
  52765. W.Add('{');
  52766. if (P=nil) or (P^<>'[') then
  52767. P := nil else
  52768. inc(P);
  52769. for i := 1 to length(Args)-1 do
  52770. if P=nil then
  52771. break else
  52772. with Args[i] do begin
  52773. if Input then begin
  52774. if ValueDirection in [smdOut,smdResult] then
  52775. continue;
  52776. end else
  52777. if ValueDirection=smdConst then
  52778. continue;
  52779. W.AddPropName(ParamName^);
  52780. P := GotoNextNotSpace(P);
  52781. Value := P;
  52782. P := GotoEndJSONItem(P);
  52783. if P^=',' then
  52784. inc(P); // include ending ','
  52785. W.AddNoJsonEscape(Value,P-Value);
  52786. end;
  52787. W.CancelLastComma;
  52788. W.Add('}');
  52789. W.SetText(result);
  52790. finally
  52791. W.Free;
  52792. end;
  52793. end;
  52794. function TServiceMethod.ArgsNames(Input: Boolean): TRawUTF8DynArray;
  52795. var a,n: integer;
  52796. begin
  52797. if Input then begin
  52798. SetLength(result,ArgsInputValuesCount);
  52799. n := 0;
  52800. for a := ArgsInFirst to ArgsInLast do
  52801. if Args[a].ValueDirection in [smdConst,smdVar] then begin
  52802. ShortStringToAnsi7String(Args[a].ParamName^,result[n]);
  52803. inc(n);
  52804. end;
  52805. end else begin
  52806. SetLength(result,ArgsOutputValuesCount);
  52807. n := 0;
  52808. for a := ArgsOutFirst to ArgsOutLast do
  52809. if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then begin
  52810. ShortStringToAnsi7String(Args[a].ParamName^,result[n]);
  52811. inc(n);
  52812. end;
  52813. end;
  52814. end;
  52815. {$ifndef NOVARIANTS}
  52816. procedure TServiceMethod.ArgsStackAsDocVariant(const Values: TPPointerDynArray;
  52817. out Dest: TDocVariantData; Input: Boolean);
  52818. var a: integer;
  52819. begin
  52820. if Input then begin
  52821. for a := ArgsInFirst to ArgsInLast do
  52822. if Args[a].ValueDirection in [smdConst,smdVar] then
  52823. Args[a].AddAsVariant(Dest,Values[a]);
  52824. end else begin
  52825. for a := ArgsOutFirst to ArgsOutLast do
  52826. if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then
  52827. Args[a].AddAsVariant(Dest,Values[a]);
  52828. end;
  52829. end;
  52830. procedure TServiceMethod.ArgsValuesAsDocVariant(Kind: TServiceMethodParamsDocVariantKind;
  52831. out Dest: TDocVariantData; const Values: TVariantDynArray; Input: boolean;
  52832. Options: TDocVariantOptions);
  52833. begin
  52834. case Kind of
  52835. pdvObject, pdvObjectFixed: begin
  52836. Dest.InitObjectFromVariants(ArgsNames(Input),Values,Options);
  52837. if Kind=pdvObjectFixed then
  52838. ArgsAsDocVariantFix(Dest,Input);
  52839. end;
  52840. pdvArray:
  52841. Dest.InitArrayFromVariants(Values,Options);
  52842. else
  52843. Dest.Init(Options);
  52844. end;
  52845. end;
  52846. procedure TServiceMethod.ArgsAsDocVariantObject(const ArgsParams: TDocVariantData;
  52847. var ArgsObject: TDocVariantData; Input: boolean);
  52848. var a,n: integer;
  52849. begin
  52850. if (ArgsParams.Count=0) or (ArgsParams.Kind<>dvArray) then
  52851. exit;
  52852. if ArgsObject.Kind=dvUndefined then
  52853. ArgsObject.Init(ArgsParams.Options);
  52854. ArgsObject.Capacity := ArgsObject.Count+ArgsParams.Count;
  52855. n := 0;
  52856. if Input then begin
  52857. if ArgsParams.Count=integer(ArgsInputValuesCount) then
  52858. for a := ArgsInFirst to ArgsInLast do
  52859. if Args[a].ValueDirection in [smdConst,smdVar] then begin
  52860. ArgsObject.AddValue(ShortStringToAnsi7String(Args[a].ParamName^),
  52861. ArgsParams.Values[n]);
  52862. inc(n);
  52863. end;
  52864. end else begin
  52865. if ArgsParams.Count=integer(ArgsOutputValuesCount) then
  52866. for a := ArgsOutFirst to ArgsOutLast do
  52867. if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then begin
  52868. ArgsObject.AddValue(ShortStringToAnsi7String(Args[a].ParamName^),
  52869. ArgsParams.Values[n]);
  52870. inc(n);
  52871. end;
  52872. end;
  52873. end;
  52874. procedure TServiceMethod.ArgsAsDocVariantFix(var ArgsObject: TDocVariantData;
  52875. Input: boolean);
  52876. var a,ndx: integer;
  52877. doc: TDocVariantData;
  52878. begin
  52879. if ArgsObject.Count>0 then
  52880. case ArgsObject.Kind of
  52881. dvObject:
  52882. for a := 0 to ArgsObject.Count-1 do begin
  52883. ndx := ArgIndex(pointer(ArgsObject.Names[a]),length(ArgsObject.Names[a]),Input);
  52884. if ndx>=0 then
  52885. Args[ndx].FixValue(ArgsObject.Values[a]);
  52886. end;
  52887. dvArray:
  52888. if Input then begin
  52889. if ArgsObject.Count<>integer(ArgsInputValuesCount) then
  52890. exit;
  52891. doc.Init(ArgsObject.Options);
  52892. for a := ArgsInFirst to ArgsInLast do
  52893. if Args[a].ValueDirection in [smdConst,smdVar] then
  52894. Args[a].FixValueAndAddToObject(ArgsObject.Values[doc.Count],doc);
  52895. ArgsObject := doc;
  52896. end else begin
  52897. if ArgsObject.Count<>integer(ArgsOutputValuesCount) then
  52898. exit;
  52899. doc.Init(ArgsObject.Options);
  52900. for a := ArgsOutFirst to ArgsOutLast do
  52901. if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then
  52902. Args[a].FixValueAndAddToObject(ArgsObject.Values[doc.Count],doc);
  52903. ArgsObject := doc;
  52904. end;
  52905. end;
  52906. end;
  52907. {$endif NOVARIANTS}
  52908. { TServiceMethodExecute }
  52909. constructor TServiceMethodExecute.Create(aMethod: PServiceMethod);
  52910. var a: integer;
  52911. begin
  52912. with aMethod^ do begin
  52913. if ArgsUsedCount[smvv64]>0 then
  52914. SetLength(fInt64s,ArgsUsedCount[smvv64]);
  52915. if ArgsUsedCount[smvvObject]>0 then
  52916. SetLength(fObjects,ArgsUsedCount[smvvObject]);
  52917. if ArgsUsedCount[smvvInterface]>0 then
  52918. SetLength(fInterfaces,ArgsUsedCount[smvvInterface]);
  52919. if ArgsUsedCount[smvvRecord]>0 then
  52920. SetLength(fRecords,ArgsUsedCount[smvvRecord]);
  52921. if ArgsUsedCount[smvvDynArray]>0 then
  52922. SetLength(fDynArrays,ArgsUsedCount[smvvDynArray]);
  52923. SetLength(fValues,length(Args));
  52924. for a := ArgsManagedFirst to ArgsManagedLast do
  52925. with Args[a] do
  52926. case ValueType of
  52927. smvDynArray:
  52928. with fDynArrays[IndexVar] do begin
  52929. Wrapper.Init(ArgTypeInfo,Value);
  52930. Wrapper.IsObjArray := vIsObjArray in ValueKindAsm; // no need to search
  52931. end;
  52932. smvRecord:
  52933. SetLength(fRecords[IndexVar],ArgTypeInfo^.RecordType^.Size);
  52934. {$ifndef NOVARIANTS}
  52935. smvVariant:
  52936. SetLength(fRecords[IndexVar],sizeof(Variant));
  52937. {$endif}
  52938. end;
  52939. end;
  52940. fMethod := aMethod;
  52941. end;
  52942. destructor TServiceMethodExecute.Destroy;
  52943. begin
  52944. fTempTextWriter.Free;
  52945. inherited Destroy;
  52946. end;
  52947. procedure TServiceMethodExecute.AddInterceptor(const Hook: TServiceMethodExecuteEvent);
  52948. begin
  52949. MultiEventAdd(fOnExecute,TMethod(Hook));
  52950. end;
  52951. procedure TServiceMethodExecute.BeforeExecute;
  52952. var a: integer;
  52953. begin
  52954. with fMethod^ do begin
  52955. if ArgsUsedCount[smvvRawUTF8]>0 then
  52956. SetLength(fRawUTF8s,ArgsUsedCount[smvvRawUTF8]);
  52957. if ArgsUsedCount[smvvString]>0 then
  52958. SetLength(fStrings,ArgsUsedCount[smvvString]);
  52959. if ArgsUsedCount[smvvWideString]>0 then
  52960. SetLength(fWideStrings,ArgsUsedCount[smvvWideString]);
  52961. if fAlreadyExecuted then begin
  52962. if ArgsUsedCount[smvvObject]>0 then
  52963. FillcharFast(fObjects,ArgsUsedCount[smvvObject]*sizeof(TObject),0);
  52964. if ArgsUsedCount[smvv64]>0 then
  52965. FillcharFast(fInt64s,ArgsUsedCount[smvv64]*sizeof(Int64),0);
  52966. if ArgsUsedCount[smvvInterface]>0 then
  52967. FillcharFast(fInterfaces,ArgsUsedCount[smvvInterface]*sizeof(pointer),0);
  52968. if ArgsUsedCount[smvvDynArray]>0 then
  52969. FillcharFast(fDynArrays,ArgsUsedCount[smvvDynArray]*sizeof(TDynArrayFake),0);
  52970. end;
  52971. for a := ArgsManagedFirst to ArgsManagedLast do
  52972. with Args[a] do
  52973. case ValueType of
  52974. smvObject:
  52975. fObjects[IndexVar] := ArgTypeInfo^.ClassCreate;
  52976. smvRecord:
  52977. if fAlreadyExecuted then
  52978. FillcharFast(fRecords[IndexVar],ArgTypeInfo^.RecordType^.Size,0);
  52979. end;
  52980. if optInterceptInputOutput in Options then begin
  52981. Input.InitFast(ArgsInputValuesCount,dvObject);
  52982. Output.InitFast(ArgsOutputValuesCount,dvObject);
  52983. end;
  52984. end;
  52985. fAlreadyExecuted := true;
  52986. end;
  52987. procedure TServiceMethodExecute.RawExecute(const Instances: PPointerArray;
  52988. InstancesLast: integer);
  52989. var Value: pointer;
  52990. a,i,e: integer;
  52991. call: TCallMethodArgs;
  52992. Stack: packed array[0..MAX_EXECSTACK-1] of byte;
  52993. begin
  52994. FillcharFast(call,SizeOf(call),0);
  52995. with fMethod^ do begin
  52996. // create the stack and register content
  52997. {$ifdef CPUX86}
  52998. call.StackAddr := PtrInt(@Stack[0]);
  52999. call.StackSize := ArgsSizeInStack;
  53000. {$else}
  53001. {$ifdef CPUINTEL}
  53002. call.StackSize := ArgsSizeInStack shr 3;
  53003. // ensure stack aligned on 16 bytes (paranoid)
  53004. if call.StackSize and 1 <> 0 then
  53005. inc(call.StackSize);
  53006. // stack is filled reversed (RTL)
  53007. call.StackAddr := PtrInt(@Stack[call.StackSize*8-8]);
  53008. {$else}
  53009. // stack is filled normally (LTR)
  53010. call.StackAddr := PtrInt(@Stack[0]);
  53011. call.StackSize := ArgsSizeInStack shr 2;
  53012. {$ifdef CPUAARCH64}
  53013. call.StackSize := ArgsSizeInStack shr 3;
  53014. // ensure stack aligned on 16 bytes (mandatory: needed for correct low level asm)
  53015. if call.StackSize and 1 <> 0 then
  53016. inc(call.StackSize);
  53017. {$endif}
  53018. {$endif CPUINTEL}
  53019. {$endif CPUX86}
  53020. for a := 1 to high(Args) do
  53021. with Args[a] do begin
  53022. case ValueVar of
  53023. smvvSelf: continue; // call.Regs[REG_FIRST] := Instance[i] below
  53024. smvv64: Value := @fInt64s[IndexVar];
  53025. smvvRawUTF8: Value := @fRawUTF8s[IndexVar];
  53026. smvvString: Value := @fStrings[IndexVar];
  53027. smvvWideString: Value := @fWideStrings[IndexVar];
  53028. smvvObject: Value := @fObjects[IndexVar];
  53029. smvvInterface: Value := @fInterfaces[IndexVar];
  53030. smvvRecord: Value := pointer(fRecords[IndexVar]);
  53031. smvvDynArray: Value := @fDynArrays[IndexVar].Value;
  53032. else raise EInterfaceFactoryException.CreateUTF8(
  53033. 'Invalid % argument type = %',[ParamName^,ord(ValueType)]);
  53034. end;
  53035. fValues[a] := Value;
  53036. if (ValueDirection<>smdConst) or
  53037. (ValueType in [smvRecord{$ifndef NOVARIANTS},smvVariant{$endif}]) then begin
  53038. // pass by reference
  53039. if (RegisterIdent=0) and (FPRegisterIdent=0) and (SizeInStack>0) then
  53040. MoveFast(Value,Stack[InStackOffset],SizeInStack) else begin
  53041. if RegisterIdent>0 then
  53042. call.ParamRegs[RegisterIdent] := PtrInt(Value);
  53043. if FPRegisterIdent>0 then
  53044. raise EInterfaceFactoryException.CreateUTF8('Unexpected % FPReg=%',
  53045. [ParamName^,FPRegisterIdent]); // should never happen
  53046. end;
  53047. end
  53048. else begin
  53049. // pass by value
  53050. if (RegisterIdent=0) AND (FPRegisterIdent=0) AND (SizeInStack>0) then
  53051. MoveFast(Value^,Stack[InStackOffset],SizeInStack) else begin
  53052. if (RegisterIdent>0) then begin
  53053. call.ParamRegs[RegisterIdent] := PPtrInt(Value)^;
  53054. {$ifdef CPUARM}
  53055. // for e.g. INT64 on 32 bit ARM systems; these are also passed in the normal registers
  53056. if SizeInStack>PTRSIZ then
  53057. call.ParamRegs[RegisterIdent+1] := PPtrInt(Value+PTRSIZ)^;
  53058. {$endif}
  53059. end;
  53060. {$ifndef CPUX86}
  53061. if FPRegisterIdent>0 then
  53062. call.FPRegs[FPRegisterIdent] := PDouble(Value)^;
  53063. {$endif}
  53064. if (RegisterIdent>0) and (FPRegisterIdent>0) then
  53065. raise EInterfaceFactoryException.CreateUTF8('Unexpected % reg=% FP=%',
  53066. [ParamName^,RegisterIdent,FPRegisterIdent]); // should never happen
  53067. end;
  53068. end;
  53069. end;
  53070. // execute the method
  53071. for i := 0 to InstancesLast do begin
  53072. // handle method execution interception
  53073. fCurrentStep := smsBefore;
  53074. if fOnExecute<>nil then begin
  53075. if (Input.Count=0) and (optInterceptInputOutput in Options) then
  53076. ArgsStackAsDocVariant(fValues,fInput,true);
  53077. for e := 0 to length(fOnExecute)-1 do
  53078. try
  53079. fOnExecute[e](self,smsBefore);
  53080. except // ignore any exception during interception
  53081. end;
  53082. end;
  53083. // prepare the low-level call context for the asm stub
  53084. {$ifndef CPUAARCH64}
  53085. call.ParamRegs[PARAMREG_FIRST] := PtrInt(Instances[i]);
  53086. {$else}
  53087. // alf note for FPC on Linux aarch64:
  53088. // the above is not true for aarch64, when a function result is a pointer
  53089. // the function result pointer is placed in REGX0 and self in REGX1
  53090. // thus, in that case: call.ParamRegs[REGX1] := PtrInt(Instances[i]);
  53091. if call.ParamRegs[PARAMREG_FIRST]=0 then
  53092. call.ParamRegs[PARAMREG_FIRST] := PtrInt(Instances[i]) else
  53093. call.ParamRegs[REGX1] := PtrInt(Instances[i]);
  53094. {$endif}
  53095. call.method := PPtrIntArray(PPointer(Instances[i])^)^[ExecutionMethodIndex];
  53096. if ArgsResultIndex>=0 then
  53097. call.resKind := Args[ArgsResultIndex].ValueType else
  53098. call.resKind := smvNone;
  53099. // launch the asm stub in the expected execution context
  53100. try
  53101. {$ifndef LVCL}
  53102. if (optExecInMainThread in Options) and
  53103. (GetCurrentThreadID<>MainThreadID) then
  53104. BackgroundExecuteCallMethod(@call,nil) else
  53105. {$endif}
  53106. if optExecInPerInterfaceThread in Options then
  53107. if Assigned(BackgroundExecutionThread) then
  53108. BackgroundExecuteCallMethod(@call,BackgroundExecutionThread) else
  53109. raise EInterfaceFactoryException.Create('optExecInPerInterfaceThread'+
  53110. ' with BackgroundExecutionThread=nil') else
  53111. CallMethod(call);
  53112. if (ArgsResultIndex>=0) and (Args[ArgsResultIndex].ValueVar=smvv64) then
  53113. PInt64Rec(fValues[ArgsResultIndex])^ := call.res64;
  53114. // handle method execution interception
  53115. fCurrentStep := smsAfter;
  53116. if fOnExecute<>nil then begin
  53117. if (Output.Count=0) and (optInterceptInputOutput in Options) then
  53118. ArgsStackAsDocVariant(fValues,fOutput,false);
  53119. for e := 0 to length(fOnExecute)-1 do
  53120. try
  53121. fOnExecute[e](self,smsAfter);
  53122. except // ignore any exception during interception
  53123. end;
  53124. end;
  53125. except // also intercept any error during method execution
  53126. on Exc: Exception do begin
  53127. if fOnExecute<>nil then begin
  53128. fCurrentStep := smsError;
  53129. fLastException := Exc;
  53130. for e := 0 to length(fOnExecute)-1 do
  53131. try
  53132. fOnExecute[e](self,smsError);
  53133. except // ignore any exception during interception
  53134. end;
  53135. fLastException := nil;
  53136. end;
  53137. raise; // caller expects the exception to be propagated
  53138. end;
  53139. end;
  53140. end;
  53141. end;
  53142. end;
  53143. function TServiceMethodExecute.TempTextWriter: TJSONSerializer;
  53144. begin
  53145. if fTempTextWriter=nil then begin
  53146. fTempTextWriter := TJSONSerializer.CreateOwnedStream;
  53147. include(fTempTextWriter.fCustomOptions,twoForceJSONExtended); // shorter
  53148. end;
  53149. result := fTempTextWriter;
  53150. end;
  53151. procedure TServiceMethodExecute.AfterExecute;
  53152. var i,a: integer;
  53153. begin
  53154. Finalize(fRawUTF8s);
  53155. Finalize(fStrings);
  53156. Finalize(fWideStrings);
  53157. with fMethod^ do
  53158. if ArgsManagedFirst>=0 then begin
  53159. for i := 0 to ArgsUsedCount[smvvObject]-1 do
  53160. fObjects[i].Free;
  53161. for i := 0 to ArgsUsedCount[smvvInterface]-1 do
  53162. IUnknown(fInterfaces[i]) := nil;
  53163. for i := 0 to ArgsUsedCount[smvvDynArray]-1 do
  53164. fDynArrays[i].Wrapper.Clear; // will handle T*ObjArray as expected
  53165. if fRecords<>nil then begin
  53166. i := 0;
  53167. for a := ArgsManagedFirst to ArgsManagedLast do
  53168. with Args[a] do
  53169. case ValueType of
  53170. smvRecord: begin
  53171. RecordClear(fRecords[i][0],ArgTypeInfo);
  53172. inc(i);
  53173. end;
  53174. {$ifndef NOVARIANTS}
  53175. smvVariant: begin
  53176. VarClear(PVariant(fRecords[i])^); // fast, even for simple types
  53177. inc(i);
  53178. end;
  53179. {$endif}
  53180. end;
  53181. end;
  53182. end;
  53183. end;
  53184. function TServiceMethodExecute.ExecuteJson(const Instances: array of pointer; Par: PUTF8Char;
  53185. Res: TTextWriter; ResAsJSONObject: boolean): boolean;
  53186. var a,a1: integer;
  53187. wasString, valid: boolean;
  53188. Val: PUTF8Char;
  53189. Name: PUTF8Char;
  53190. NameLen: integer;
  53191. EndOfObject: AnsiChar;
  53192. ParObjValues: TPUTF8CharDynArray;
  53193. begin
  53194. //alfchange
  53195. ParObjValues:=nil;
  53196. result := false;
  53197. if high(Instances)<0 then
  53198. exit;
  53199. BeforeExecute;
  53200. with fMethod^ do
  53201. try
  53202. // validate input parameters
  53203. if (ArgsInputValuesCount<>0) and (Par<>nil) then begin
  53204. if Par^ in [#1..' '] then repeat inc(Par) until not(Par^ in [#1..' ']);
  53205. case Par^ of
  53206. '[': // input arguments as a JSON array , e.g. '[1,2,"three"]' (default)
  53207. inc(Par);
  53208. '{': begin // retrieve parameters values from JSON object
  53209. inc(Par);
  53210. SetLength(ParObjValues,ArgsInLast+1); // nil will set default value
  53211. a1 := ArgsInFirst;
  53212. repeat
  53213. Name := GetJSONPropName(Par);
  53214. if Name=nil then
  53215. exit; // invalid JSON object in input
  53216. NameLen := StrLen(Name);
  53217. Val := Par;
  53218. Par := GotoNextJSONItem(Par,1,@EndOfObject);
  53219. for a := a1 to ArgsInLast do
  53220. with Args[a] do
  53221. if ValueDirection<>smdOut then
  53222. if IdemPropName(ParamName^,Name,NameLen) then begin
  53223. ParObjValues[a] := Val; // fast redirection, without allocation
  53224. if a=a1 then
  53225. inc(a1); // enable optimistic O(1) search for in-order input
  53226. break;
  53227. end;
  53228. until (Par=nil) or (EndOfObject='}');
  53229. Par := nil;
  53230. end;
  53231. else exit; // only support JSON array or JSON object as input
  53232. end;
  53233. end;
  53234. // decode input parameters (if any) in f*[]
  53235. if (Par=nil) and (ParObjValues=nil) then begin
  53236. if (ArgsInputValuesCount>0) and (optErrorOnMissingParam in Options) then
  53237. exit; // paranoid setting
  53238. end else
  53239. for a := ArgsInFirst to ArgsInLast do
  53240. with Args[a] do
  53241. if ValueDirection<>smdOut then begin
  53242. if ParObjValues<>nil then
  53243. if ParObjValues[a]=nil then // missing parameter in input JSON
  53244. if optErrorOnMissingParam in Options then
  53245. exit else // paranoid setting
  53246. continue else // ignore and use void value by default
  53247. Par := ParObjValues[a]; // value is to be retrieved from JSON object
  53248. case ValueType of
  53249. smvObject: begin
  53250. Par := JSONToObject(fObjects[IndexVar],Par,valid,nil,JSONTOOBJECT_TOLERANTOPTIONS);
  53251. if not valid then
  53252. exit;
  53253. IgnoreComma(Par);
  53254. end;
  53255. smvInterface:
  53256. if Assigned(OnCallback) then
  53257. OnCallback(Par,ArgTypeInfo,fInterfaces[IndexVar]) else
  53258. raise EInterfaceFactoryException.CreateUTF8(
  53259. 'Unhandled %(%: %) parameter',[URI,ParamName^,ArgTypeName^]);
  53260. smvRawJSON:
  53261. GetJSONItemAsRawJSON(Par,RawJSON(fRawUTF8s[IndexVar]));
  53262. smvDynArray: begin
  53263. Par := fDynArrays[IndexVar].Wrapper.LoadFromJSON(Par);
  53264. IgnoreComma(Par);
  53265. end;
  53266. smvRecord:
  53267. Par := RecordLoadJSON(pointer(fRecords[IndexVar])^,Par,ArgTypeInfo);
  53268. {$ifndef NOVARIANTS}
  53269. smvVariant:
  53270. Par := VariantLoadJSON(PVariant(pointer(fRecords[IndexVar]))^,Par,nil,
  53271. @JSON_OPTIONS[optVariantCopiedByReference in Options]);
  53272. {$endif}
  53273. smvBoolean..smvWideString: begin
  53274. Val := GetJSONField(Par,Par,@wasString,@EndOfObject);
  53275. if (Val=nil) and (Par=nil) and (EndOfObject<>'}') then
  53276. exit; // 'null' will set Val=nil and Par<>nil
  53277. if (Val<>nil) and (wasString and not (vIsString in ValueKindAsm)) then
  53278. exit;
  53279. case ValueType of
  53280. smvBoolean:
  53281. fInt64s[IndexVar] := byte((Val<>nil) and
  53282. ((PWord(Val)^=ord('1'))or(PInteger(Val)^=TRUE_LOW)));
  53283. smvEnum..smvInt64:
  53284. SetInt64(Val,fInt64s[IndexVar]);
  53285. smvDouble,smvDateTime:
  53286. PDouble(@fInt64s[IndexVar])^ := GetExtended(Val);
  53287. smvCurrency:
  53288. fInt64s[IndexVar] := StrToCurr64(Val);
  53289. smvRawUTF8:
  53290. SetString(fRawUTF8s[IndexVar],Val,StrLen(Val));
  53291. smvString:
  53292. UTF8DecodeToString(Val,StrLen(Val),fStrings[IndexVar]);
  53293. smvRawByteString:
  53294. Base64ToBin(PAnsiChar(Val),StrLen(Val),RawByteString(fRawUTF8s[IndexVar]));
  53295. smvWideString:
  53296. UTF8ToWideString(Val,StrLen(Val),fWideStrings[IndexVar]);
  53297. else exit; // should not happen
  53298. end;
  53299. continue; // here Par=nil or Val=nil is correct
  53300. end;
  53301. else continue;
  53302. end;
  53303. if Par=nil then
  53304. exit;
  53305. end;
  53306. // execute the method, using prepared values in f*[]
  53307. RawExecute(@Instances[0],high(Instances));
  53308. // send back any result
  53309. if Res<>nil then begin
  53310. // handle custom content (not JSON array/object answer)
  53311. if ArgsResultIsServiceCustomAnswer then
  53312. with PServiceCustomAnswer(fValues[ArgsResultIndex])^ do
  53313. if Header<>'' then begin
  53314. fServiceCustomAnswerHead := Header;
  53315. Res.ForceContent(Content);
  53316. if Status=0 then // Values[]=@Records[] is filled with 0 by default
  53317. fServiceCustomAnswerStatus := HTML_SUCCESS else
  53318. fServiceCustomAnswerStatus := Status;
  53319. Result := true;
  53320. exit;
  53321. end;
  53322. // write the '{"result":[...' array or object
  53323. for a := ArgsOutFirst to ArgsOutLast do
  53324. with Args[a] do
  53325. if ValueDirection in [smdVar,smdOut,smdResult] then begin
  53326. if ResAsJSONObject then
  53327. Res.AddPropName(ParamName^);
  53328. AddJSON(Res,fValues[a]);
  53329. end;
  53330. Res.CancelLastComma;
  53331. end;
  53332. Result := true;
  53333. finally
  53334. Finalize(ParObjValues);
  53335. AfterExecute;
  53336. end;
  53337. end;
  53338. { TSQLRecordServiceLog }
  53339. class procedure TSQLRecordServiceLog.InitializeTable(
  53340. Server: TSQLRestServer; const FieldName: RawUTF8;
  53341. Options: TSQLInitializeTableOptions);
  53342. begin
  53343. inherited;
  53344. if FieldName='' then
  53345. Server.CreateSQLMultiIndex(Self,['Method','MicroSec'],false);
  53346. end;
  53347. class procedure TSQLRecordServiceLog.InternalDefineModel(Props: TSQLRecordProperties);
  53348. begin
  53349. Props.SetVariantFieldsDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED);
  53350. Props.SetCustomCollationForAll(sftUTF8Text,'NOCASE'); // slightly faster
  53351. end;
  53352. { TSQLRecordServiceNotifications }
  53353. class procedure TSQLRecordServiceNotifications.InitializeTable(
  53354. Server: TSQLRestServer; const FieldName: RawUTF8;
  53355. Options: TSQLInitializeTableOptions);
  53356. begin
  53357. inherited;
  53358. if (FieldName='') or (FieldName='Sent') then
  53359. Server.CreateSQLMultiIndex(Self,['Sent'],false);
  53360. end;
  53361. class function TSQLRecordServiceNotifications.LastEventsAsObjects(Rest: TSQLRest;
  53362. LastKnownID: TID; Limit: integer; Service: TInterfaceFactory; out Dest: TDocVariantData;
  53363. const MethodName: RawUTF8; IDAsHexa: boolean): boolean;
  53364. var res: TSQLRecordServiceNotifications;
  53365. begin
  53366. res := CreateAndFillPrepare(Rest,'ID > ? order by ID limit %',[Limit],
  53367. [LastKnownID],'ID,Method,Input');
  53368. try
  53369. if res.FillTable.RowCount > 0 then begin
  53370. res.SaveFillInputsAsObjects(Service,Dest,MethodName,IDAsHexa);
  53371. result := true;
  53372. end else
  53373. result := false;
  53374. finally
  53375. res.Free;
  53376. end;
  53377. end;
  53378. function TSQLRecordServiceNotifications.SaveInputAsObject(Service: TInterfaceFactory;
  53379. const MethodName: RawUTF8; IDAsHexa: boolean): variant;
  53380. var m: integer;
  53381. begin
  53382. VarClear(result);
  53383. with TDocVariantData(result) do
  53384. if IDAsHexa then
  53385. InitObject(['ID',Int64ToHex(fID),MethodName,Method],JSON_OPTIONS_FAST) else
  53386. InitObject(['ID',fID,MethodName,Method],JSON_OPTIONS_FAST);
  53387. m := Service.FindMethodIndex(Method);
  53388. if m>=0 then
  53389. Service.Methods[m].ArgsAsDocVariantObject(_Safe(fInput)^,TDocVariantData(result),true);
  53390. end;
  53391. procedure TSQLRecordServiceNotifications.SaveFillInputsAsObjects(Service: TInterfaceFactory;
  53392. out Dest: TDocVariantData; const MethodName: RawUTF8; IDAsHexa: boolean);
  53393. begin
  53394. Dest.InitFast(FillTable.RowCount,dvArray);
  53395. while FillOne do
  53396. Dest.AddItem(SaveInputAsObject(Service,MethodName,IDAsHexa));
  53397. end;
  53398. { TServiceContainerClient }
  53399. function TServiceContainerClient.Info(aTypeInfo: PTypeInfo): TServiceFactory;
  53400. begin
  53401. result := inherited Info(aTypeInfo);
  53402. if (result=nil) and (not fDisableAutoRegisterAsClientDriven) then
  53403. result := AddInterface(aTypeInfo,sicClientDriven);
  53404. end;
  53405. function TServiceContainerClient.CallBackUnRegister(const Callback: IInvokable): boolean;
  53406. begin
  53407. if Assigned(Callback) then
  53408. result := (fRest as TSQLRestClientURI).fFakeCallbacks.UnRegister(pointer(Callback)) else
  53409. result := false;
  53410. end;
  53411. { TInterfacedCallback }
  53412. constructor TInterfacedCallback.Create(aRest: TSQLRest; const aGUID: TGUID);
  53413. begin
  53414. inherited Create;
  53415. fRest := aRest;
  53416. fInterface := aGUID;
  53417. end;
  53418. procedure TInterfacedCallback.CallbackRestUnregister;
  53419. var Obj: pointer; // to avoid unexpected (recursive) Destroy call
  53420. begin
  53421. if (fRest<>nil) and (fRest.Services<>nil) and not IsNullGUID(fInterface) then
  53422. if GetInterface(fInterface,Obj) then begin
  53423. fRest.Services.CallBackUnRegister(IInvokable(Obj));
  53424. dec(fRefCount); // GetInterface() did increase the refcount
  53425. fRest := nil; // notify once
  53426. end;
  53427. end;
  53428. destructor TInterfacedCallback.Destroy;
  53429. begin
  53430. CallbackRestUnregister;
  53431. inherited Destroy;
  53432. end;
  53433. { TBlockingCallback }
  53434. constructor TBlockingCallback.Create(aTimeOutMs: integer;
  53435. aRest: TSQLRest; const aGUID: TGUID);
  53436. begin
  53437. inherited Create(aRest,aGUID);
  53438. fProcess := TBlockingProcess.Create(aTimeOutMs,fSafe);
  53439. end;
  53440. destructor TBlockingCallback.Destroy;
  53441. begin
  53442. FreeAndNil(fProcess);
  53443. inherited Destroy;
  53444. end;
  53445. procedure TBlockingCallback.CallbackFinished(aRestForLog: TSQLRest;
  53446. aServerUnregister: boolean);
  53447. begin
  53448. if fProcess.NotifyFinished then begin
  53449. {$ifdef WITHLOG}
  53450. if aRestForLog<>nil then
  53451. aRestForLog.LogClass.Add.Log(sllTrace,self);
  53452. {$endif}
  53453. if aServerUnregister then
  53454. CallbackRestUnregister;
  53455. end;
  53456. end;
  53457. function TBlockingCallback.WaitFor: TBlockingEvent;
  53458. begin
  53459. result := fProcess.WaitFor;
  53460. end;
  53461. function TBlockingCallback.Reset: boolean;
  53462. begin
  53463. result := fProcess.Reset;
  53464. end;
  53465. function TBlockingCallback.GetEvent: TBlockingEvent;
  53466. begin
  53467. result := fProcess.Event;
  53468. end;
  53469. { TServiceRecordVersionCallback }
  53470. constructor TServiceRecordVersionCallback.Create(aSlave: TSQLRestServer;
  53471. aMaster: TSQLRestClientURI; aTable: TSQLRecordClass; aOnNotify: TOnBatchWrite);
  53472. begin
  53473. if aSlave=nil then
  53474. raise EServiceException.CreateUTF8('%.Create(%): Slave=nil',[self,aTable]);
  53475. fSlave := aSlave;
  53476. fRecordVersionField := aTable.RecordProps.RecordVersionField;
  53477. if fRecordVersionField=nil then
  53478. raise EServiceException.CreateUTF8('%.Create: % has no TRecordVersion field',
  53479. [self,aTable]);
  53480. fTableDeletedIDOffset := Int64(fSlave.Model.GetTableIndexExisting(aTable))
  53481. shl SQLRECORDVERSION_DELETEID_SHIFT;
  53482. inherited Create(aMaster,IServiceRecordVersionCallback);
  53483. fTable := aTable;
  53484. fOnNotify := aOnNotify;
  53485. end;
  53486. procedure TServiceRecordVersionCallback.SetCurrentRevision(
  53487. const Revision: TRecordVersion; Event: TSQLOccasion);
  53488. begin
  53489. if (Revision<fSlave.fRecordVersionMax) or
  53490. ((Revision=fSlave.fRecordVersionMax) and (Event<>soInsert)) then
  53491. raise EServiceException.CreateUTF8('%.SetCurrentRevision(%) on %: previous was %',
  53492. [self,Revision,fTable,fSlave.fRecordVersionMax]);
  53493. fSlave.fRecordVersionMax := Revision;
  53494. end;
  53495. procedure TServiceRecordVersionCallback.Added(const NewContent: RawJSON);
  53496. var rec: TSQLRecord;
  53497. fields: TSQLFieldBits;
  53498. begin
  53499. rec := fTable.Create;
  53500. try
  53501. rec.FillFrom(NewContent,@fields);
  53502. if fBatch=nil then
  53503. fSlave.Add(rec,true,true,true) else
  53504. fBatch.Add(rec,true,true,fields,true);
  53505. SetCurrentRevision(fRecordVersionField.PropInfo.GetInt64Prop(rec),soInsert);
  53506. if Assigned(fOnNotify) then
  53507. fOnNotify(fBatch,soInsert,fTable,rec.IDValue,rec,fields);
  53508. finally
  53509. rec.Free;
  53510. end;
  53511. end;
  53512. procedure TServiceRecordVersionCallback.Updated(const ModifiedContent: RawJSON);
  53513. var rec: TSQLRecord;
  53514. fields: TSQLFieldBits;
  53515. begin
  53516. rec := fTable.Create;
  53517. try
  53518. rec.FillFrom(ModifiedContent,@fields);
  53519. if fBatch=nil then
  53520. fSlave.Update(rec,fields,true) else
  53521. fBatch.Update(rec,fields,true);
  53522. SetCurrentRevision(fRecordVersionField.PropInfo.GetInt64Prop(rec),soUpdate);
  53523. if Assigned(fOnNotify) then
  53524. fOnNotify(fBatch,soUpdate,fTable,rec.IDValue,rec,fields);
  53525. finally
  53526. rec.Free;
  53527. end;
  53528. end;
  53529. procedure TServiceRecordVersionCallback.Deleted(const ID: TID;
  53530. const Revision: TRecordVersion);
  53531. var del: TSQLRecordTableDeleted;
  53532. begin
  53533. del := TSQLRecordTableDeleted.Create;
  53534. try
  53535. del.IDValue := fTableDeletedIDOffset+Revision;
  53536. del.Deleted := ID;
  53537. if fBatch=nil then
  53538. try
  53539. fSlave.fAcquireExecution[execORMWrite].fSafe.Lock;
  53540. fSlave.fRecordVersionDeleteIgnore := true;
  53541. fSlave.Add(del,true,true,true);
  53542. fSlave.Delete(fTable,ID);
  53543. finally
  53544. fSlave.fRecordVersionDeleteIgnore := false;
  53545. fSlave.fAcquireExecution[execORMWrite].Safe.UnLock;
  53546. end else begin
  53547. fBatch.Add(del,true,true);
  53548. fBatch.Delete(fTable,ID);
  53549. end;
  53550. SetCurrentRevision(Revision,soDelete);
  53551. if Assigned(fOnNotify) then
  53552. fOnNotify(fBatch,soDelete,fTable,ID,nil,[]);
  53553. finally
  53554. del.Free;
  53555. end;
  53556. end;
  53557. procedure TServiceRecordVersionCallback.CurrentFrame(isLast: boolean);
  53558. procedure Error(const msg: RawUTF8);
  53559. begin
  53560. fRest.InternalLog('%.CurrentFrame(%) on %: %',[self,isLast,fTable,msg],sllError);
  53561. end;
  53562. begin
  53563. if isLast then begin
  53564. if fBatch=nil then
  53565. Error('unexpected last frame');
  53566. end else
  53567. if fBatch<>nil then
  53568. Error('previous active BATCH -> send pending');
  53569. if fBatch<>nil then
  53570. try
  53571. fSlave.fAcquireExecution[execORMWrite].fSafe.Lock;
  53572. fSlave.fRecordVersionDeleteIgnore := true;
  53573. fSlave.BatchSend(fBatch);
  53574. finally
  53575. fSlave.fRecordVersionDeleteIgnore := false;
  53576. fSlave.fAcquireExecution[execORMWrite].Safe.UnLock;
  53577. FreeAndNil(fBatch);
  53578. end;
  53579. if not isLast then
  53580. fBatch := TSQLRestBatch.Create(fSlave,nil,10000);
  53581. end;
  53582. destructor TServiceRecordVersionCallback.Destroy;
  53583. var timeOut: Int64;
  53584. begin
  53585. try
  53586. if fBatch<>nil then begin
  53587. timeOut := GetTickCount64+2000;
  53588. repeat
  53589. sleep(1); // allow 2 seconds to process all pending frames
  53590. if fBatch=nil then
  53591. exit;
  53592. until GetTickCount64>timeOut;
  53593. fSlave.InternalLog('%.Destroy on %: active BATCH',[self,fTable],sllError);
  53594. fSlave.BatchSend(fBatch);
  53595. fBatch.Free;
  53596. end;
  53597. finally
  53598. inherited Destroy;
  53599. end;
  53600. end;
  53601. { TServiceFactoryClient }
  53602. function TServiceFactoryClient.CreateFakeInstance: TInterfacedObject;
  53603. var notify: TOnFakeInstanceDestroy;
  53604. begin
  53605. if fInstanceCreation=sicClientDriven then
  53606. notify := NotifyInstanceDestroyed else
  53607. notify := nil;
  53608. result := TInterfacedObjectFakeClient.Create(self,Invoke,notify);
  53609. end;
  53610. type
  53611. TServiceFactoryClientNotificationThread = class(TSQLRestThread)
  53612. protected
  53613. fClient: TServiceFactoryClient;
  53614. fRemote: TSQLRestClientURI;
  53615. fRetryPeriodSeconds: Integer;
  53616. fPending: integer;
  53617. procedure InternalExecute; override;
  53618. procedure ProcessPendingNotification;
  53619. function GetPendingCountFromDB: Int64;
  53620. public
  53621. constructor Create(aClient: TServiceFactoryClient; aRemote: TSQLRestClientURI;
  53622. aRetryPeriodSeconds: Integer); reintroduce;
  53623. end;
  53624. constructor TServiceFactoryClientNotificationThread.Create(
  53625. aClient: TServiceFactoryClient; aRemote: TSQLRestClientURI; aRetryPeriodSeconds: Integer);
  53626. begin
  53627. fClient := aClient; // cross-platform may run Execute as soon as Create is called
  53628. if (fClient=nil) or (fClient.fSendNotificationsRest=nil) or
  53629. (fClient.fSendNotificationsLogClass=nil) then
  53630. raise EServiceException.CreateUTF8('%.Create(fClient.fSendNotifications=nil)',[self]);
  53631. if aRetryPeriodSeconds<=0 then
  53632. fRetryPeriodSeconds := 1 else
  53633. fRetryPeriodSeconds := aRetryPeriodSeconds;
  53634. if aRemote=nil then
  53635. fRemote := fClient.fClient else
  53636. fRemote := aRemote;
  53637. fPending := GetPendingCountFromDB;
  53638. inherited Create(fClient.fClient,false,false);
  53639. end;
  53640. function TServiceFactoryClientNotificationThread.GetPendingCountFromDB: Int64;
  53641. begin
  53642. if not fClient.fSendNotificationsRest.OneFieldValue(
  53643. fClient.fSendNotificationsLogClass,'count(*)','Sent=?',[],[0],result) then
  53644. result := 0;
  53645. end;
  53646. procedure TServiceFactoryClientNotificationThread.ProcessPendingNotification;
  53647. var pending: TSQLRecordServiceNotifications;
  53648. params,error: RawUTF8;
  53649. client: cardinal;
  53650. count: integer;
  53651. timer: TPrecisionTimer;
  53652. begin // one at a time, since InternalInvoke() is the bottleneck
  53653. pending := fClient.fSendNotificationsLogClass.Create(
  53654. fClient.fSendNotificationsRest,'Sent=? order by id limit 1',[0]);
  53655. try
  53656. if pending.IDValue=0 then begin
  53657. fPending := GetPendingCountFromDB;
  53658. if fPending=0 then
  53659. exit else
  53660. raise EServiceException.CreateUTF8(
  53661. '%.ProcessPendingNotification pending=% with no DB row',[self,fPending]);
  53662. end;
  53663. timer.Start;
  53664. VariantSaveJson(pending.Input,twJSONEscape,params);
  53665. if (params<>'') and (params[1]='[') then
  53666. params := copy(params,2,length(params)-2); // trim [..] for URI call
  53667. client := pending.Session;
  53668. if not fClient.InternalInvoke(pending.Method,params,nil,@error,@client,nil,fRemote) then begin
  53669. if _Safe(pending.fOutput)^.GetAsInteger('errorcount',count) then
  53670. inc(count) else
  53671. count := 1;
  53672. VarClear(pending.fOutput);
  53673. TDocVariantData(pending.fOutput).InitObject(['errorcount',count,
  53674. 'lasterror',error,'lasttime',NowUTCToString(true,'T'),
  53675. 'lastelapsed',timer.Stop],JSON_OPTIONS_FAST_EXTENDED);
  53676. fClient.fSendNotificationsRest.Update(pending,'Output',true);
  53677. raise EServiceException.CreateUTF8(
  53678. '%.ProcessPendingNotification failed for %(%) [ID=%,pending=%] on %: %',
  53679. [self,pending.Method,params,pending.IDValue,fPending,fRemote,error]);
  53680. end;
  53681. fClient.fClient.InternalLog('ProcessPendingNotification %(%) in % [ID=%,pending=%]',
  53682. [pending.Method,params,timer.Stop,pending.IDValue,fPending],sllTrace);
  53683. pending.Sent := TimeLogNowUTC;
  53684. pending.MicroSec := timer.LastTimeInMicroSec;
  53685. fClient.fSendNotificationsRest.Update(pending,'MicroSec,Sent',true);
  53686. InterlockedDecrement(fPending);
  53687. finally
  53688. pending.Free;
  53689. end;
  53690. end;
  53691. procedure TServiceFactoryClientNotificationThread.InternalExecute;
  53692. var delay: integer;
  53693. begin
  53694. delay := 50;
  53695. while not Terminated do begin
  53696. while fPending>0 do
  53697. try
  53698. ProcessPendingNotification;
  53699. delay := 0;
  53700. if Terminated then
  53701. exit;
  53702. except
  53703. SleepOrTerminated(fRetryPeriodSeconds*1000); // wait before retry
  53704. end;
  53705. if Terminated then
  53706. exit;
  53707. if delay<50 then
  53708. inc(delay);
  53709. SleepHiRes(delay);
  53710. end;
  53711. end;
  53712. function TServiceFactoryClient.Invoke(const aMethod: TServiceMethod;
  53713. const aParams: RawUTF8; aResult: PRawUTF8; aErrorMsg: PRawUTF8;
  53714. aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean;
  53715. procedure SendNotificationsLog;
  53716. var pending: TSQLRecordServiceNotifications;
  53717. json: RawUTF8;
  53718. begin
  53719. pending := fSendNotificationsLogClass.Create;
  53720. try
  53721. pending.Method := aMethod.URI;
  53722. json := '['+aParams+']';
  53723. TDocVariantData(pending.fInput).InitJSONInPlace(pointer(json),JSON_OPTIONS_FAST_EXTENDED);
  53724. if aClientDrivenID<>nil then
  53725. pending.Session := aClientDrivenID^;
  53726. fSendNotificationsRest.Add(pending,true);
  53727. finally
  53728. pending.Free;
  53729. end;
  53730. end;
  53731. begin
  53732. if (fSendNotificationsRest<>nil) and (aMethod.ArgsOutputValuesCount=0) then begin
  53733. SendNotificationsLog;
  53734. if fSendNotificationsThread<>nil then
  53735. InterlockedIncrement(TServiceFactoryClientNotificationThread(
  53736. fSendNotificationsThread).fPending);
  53737. result := true;
  53738. end else
  53739. result := InternalInvoke(
  53740. aMethod.URI,aParams,aResult,aErrorMsg,aClientDrivenID,aServiceCustomAnswer);
  53741. end;
  53742. class function TServiceFactoryClient.GetErrorMessage(status: integer): RawUTF8;
  53743. begin
  53744. case status of
  53745. HTML_UNAVAILABLE: result := 'Check the communication parameters';
  53746. HTML_NOTIMPLEMENTED: result := 'Server not reachable';
  53747. HTML_NOTALLOWED: result := 'Method forbidden for this User group';
  53748. HTML_UNAUTHORIZED: result := 'No active session';
  53749. HTML_NOTACCEPTABLE: result := 'Invalid input parameters';
  53750. else result := '';
  53751. end;
  53752. end;
  53753. function TServiceFactoryClient.InternalInvoke(const aMethod: RawUTF8;
  53754. const aParams: RawUTF8; aResult: PRawUTF8; aErrorMsg: PRawUTF8;
  53755. aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer;
  53756. aClient: TSQLRestClientURI): boolean;
  53757. var uri,sent,resp,head,clientDrivenID: RawUTF8;
  53758. Values: TPUtf8CharDynArray;
  53759. status,m: integer;
  53760. {$ifdef WITHLOG}
  53761. Log: ISynLog; // for Enter auto-leave to work with FPC
  53762. p: RawUTF8;
  53763. {$endif}
  53764. begin
  53765. result := false;
  53766. if Self=nil then
  53767. exit;
  53768. if fClient=nil then
  53769. fClient := fRest as TSQLRestClientURI;
  53770. if aClient=nil then
  53771. aClient := fClient;
  53772. if (aClientDrivenID<>nil) and (aClientDrivenID^>0) then
  53773. UInt32ToUTF8(aClientDrivenID^,clientDrivenID);
  53774. m := fInterface.FindMethodIndex(aMethod);
  53775. {$ifdef WITHLOG}
  53776. if (m<0) or not (optNoLogInput in fExecution[m].Options) then
  53777. p := aParams else
  53778. p := 'optNoLogInput';
  53779. Log := fRest.LogClass.Enter('InternalInvoke I%.%(%) %',
  53780. [fInterfaceURI,aMethod,p,clientDrivenID],self);
  53781. {$endif}
  53782. // compute URI according to current routing scheme
  53783. if fForcedURI<>'' then
  53784. uri := fForcedURI else
  53785. if fRest.Services.ExpectMangledURI then
  53786. uri := aClient.Model.Root+'/'+fInterfaceMangledURI else
  53787. uri := aClient.Model.Root+'/'+fInterfaceURI;
  53788. fRest.ServicesRouting.ClientSideInvoke(uri,aMethod,aParams,clientDrivenID,sent);
  53789. if ParamsAsJSONObject and (clientDrivenID='') then
  53790. if m>=0 then // ParamsAsJSONObject won't apply to _signature_ e.g.
  53791. sent := fInterface.Methods[m].ArgsArrayToObject(Pointer(sent),true);
  53792. // call remote server
  53793. status := aClient.URI(uri,'POST',@resp,@head,@sent).Lo;
  53794. // decode result
  53795. if aServiceCustomAnswer=nil then begin
  53796. // handle errors at REST level
  53797. if not StatusCodeIsSuccess(status) then begin
  53798. if aErrorMsg<>nil then begin
  53799. if resp='' then begin
  53800. StatusCodeToErrorMsg(status,resp);
  53801. head := GetErrorMessage(status);
  53802. if head<>'' then
  53803. head := ' - '+head;
  53804. aErrorMsg^ := FormatUTF8('URI % % returned status ''%'' (%%)',
  53805. [uri,sent,resp,status,head]);
  53806. end else
  53807. aErrorMsg^ := resp;
  53808. end;
  53809. exit; // leave result=false
  53810. end;
  53811. // decode JSON object
  53812. {$ifdef WITHLOG}
  53813. if (m<0) or not (optNoLogOutput in fExecution[m].Options) then
  53814. with fRest.fLogFamily do
  53815. if (sllServiceReturn in Level) and (resp<>'') then
  53816. SynLog.Log(sllServiceReturn,resp,self,MAX_SIZE_RESPONSE_LOG);
  53817. {$endif}
  53818. if fResultAsJSONObject then begin
  53819. if aResult<>nil then
  53820. aResult^ := resp;
  53821. if aClientDrivenID<>nil then
  53822. aClientDrivenID^ := 0;
  53823. end else
  53824. if (resp<>'') and (aClientDrivenID=nil) and
  53825. not IdemPChar(GotoNextNotSpace(pointer(resp)),'{"RESULT":') then begin
  53826. if aResult<>nil then
  53827. aResult^ := resp; // e.g. when client retrieves the contract
  53828. end else begin
  53829. JSONDecode(pointer(resp),['result','id'],Values,True);
  53830. if Values[0]=nil then begin // no "result":... layout
  53831. if aErrorMsg<>nil then
  53832. aErrorMsg^ :=
  53833. 'Invalid returned JSON content: expects {"result":...}, got '+resp;
  53834. exit; // leave result=false
  53835. end;
  53836. if aResult<>nil then
  53837. SetString(aResult^,Values[0],StrLen(Values[0]));
  53838. if aClientDrivenID<>nil then // assume ID=0 if no "id":... value
  53839. aClientDrivenID^ := GetCardinal(Values[1]);
  53840. end;
  53841. end else begin
  53842. // custom answer returned in TServiceCustomAnswer
  53843. fRest.InternalLog('TServiceCustomAnswer(%) returned status=% len=%',
  53844. [head,status,length(resp)],sllServiceReturn);
  53845. aServiceCustomAnswer^.Status := status;
  53846. aServiceCustomAnswer^.Header := head;
  53847. aServiceCustomAnswer^.Content := resp;
  53848. if aClientDrivenID<>nil then
  53849. aClientDrivenID^ := 0;
  53850. end;
  53851. result := true;
  53852. end;
  53853. procedure TServiceFactoryClient.NotifyInstanceDestroyed(aClientDrivenID: cardinal);
  53854. begin
  53855. if aClientDrivenID<>0 then
  53856. InternalInvoke(SERVICE_PSEUDO_METHOD[imFree],'',nil,nil,@aClientDrivenID);
  53857. end;
  53858. constructor TServiceFactoryClient.Create(aRest: TSQLRest;
  53859. aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation;
  53860. const aContractExpected: RawUTF8);
  53861. var Error, RemoteContract: RawUTF8;
  53862. begin
  53863. // extract interface RTTI and create fake interface (and any shared instance)
  53864. if not aRest.InheritsFrom(TSQLRestClientURI) then
  53865. EServiceException.CreateUTF8('%.Create(): % interface needs a Client connection',
  53866. [self,aInterface^.Name]);
  53867. inherited Create(aRest,aInterface,aInstanceCreation,aContractExpected);
  53868. // initialize a shared instance (if needed)
  53869. if fInstanceCreation in [sicShared,sicPerSession,sicPerUser,sicPerGroup,sicPerThread] then begin
  53870. // the instance shall remain active during the whole client session
  53871. fSharedInstance := CreateFakeInstance;
  53872. TInterfacedObjectFake(fSharedInstance)._AddRef; // force stay alive
  53873. end;
  53874. // check if this interface is supported on the server
  53875. if ContractExpected<>SERVICE_CONTRACT_NONE_EXPECTED then begin
  53876. if not InternalInvoke(SERVICE_PSEUDO_METHOD[imContract],
  53877. TSQLRestClientURI(fRest).fServicePublishOwnInterfaces,@RemoteContract,@Error) then
  53878. raise EServiceException.CreateUTF8('%.Create(): I% interface or % routing not '+
  53879. 'supported by server: %',[self,fInterfaceURI,fRest.ServicesRouting,Error]);
  53880. if ('['+ContractExpected+']'<>RemoteContract) and
  53881. ('{"contract":'+ContractExpected+'}'<>RemoteContract) then
  53882. raise EServiceException.CreateUTF8('%.Create(): server''s I% contract '+
  53883. 'differs from client''s: expected [%], received %',
  53884. [self,fInterfaceURI,ContractExpected,RemoteContract]);
  53885. end;
  53886. end;
  53887. destructor TServiceFactoryClient.Destroy;
  53888. begin
  53889. FreeAndNil(fSendNotificationsThread);
  53890. if fSharedInstance<>nil then
  53891. with TInterfacedObjectFake(fSharedInstance) do
  53892. if fRefCount<>1 then
  53893. raise EServiceException.CreateUTF8('%.Destroy with RefCount=%: you must release '+
  53894. 'I% interface (setting := nil) before Client.Free',[self,fRefCount,fInterfaceURI]) else
  53895. _Release; // bonne nuit les petits
  53896. inherited;
  53897. end;
  53898. function TServiceFactoryClient.RetrieveSignature: RawUTF8;
  53899. begin
  53900. result := '';
  53901. if InternalInvoke(SERVICE_PSEUDO_METHOD[imSignature],'',@result) and
  53902. (result<>'') then
  53903. if result[1]='[' then
  53904. result := copy(result,2,length(result)-2) else
  53905. if IdemPChar(pointer(result),'{"SIGNATURE":') then
  53906. result := copy(result,14,length(result)-14);
  53907. end;
  53908. function TServiceFactoryClient.Get(out Obj): Boolean;
  53909. var O: TInterfacedObjectFake;
  53910. begin
  53911. result := false;
  53912. if Self=nil then
  53913. exit;
  53914. case fInstanceCreation of
  53915. sicShared, sicPerSession, sicPerUser, sicPerGroup, sicPerThread:
  53916. O := TInterfacedObjectFake(fSharedInstance);
  53917. sicSingle, sicClientDriven:
  53918. O := TInterfacedObjectFake(CreateFakeInstance);
  53919. else exit;
  53920. end;
  53921. if O=nil then
  53922. exit;
  53923. pointer(Obj) := @O.fVTable;
  53924. O._AddRef;
  53925. result := true;
  53926. end;
  53927. procedure TServiceFactoryClient.StoreNotifications(aRest: TSQLRest;
  53928. aLogClass: TSQLRecordServiceNotificationsClass);
  53929. var c: TClass;
  53930. begin
  53931. if (aRest=fSendNotificationsRest) and (aLogClass=fSendNotificationsLogClass) then
  53932. exit;
  53933. fSendNotificationsRest := aRest;
  53934. fSendNotificationsLogClass := aLogClass;
  53935. if aRest=nil then
  53936. c := nil else
  53937. c := aRest.ClassType;
  53938. fClient.InternalLog('%.StoreNotifications(%,%) for I%',
  53939. [ClassType,c,aLogClass,fInterfaceURI],sllTrace);
  53940. end;
  53941. procedure TServiceFactoryClient.SendNotifications(aRest: TSQLRest;
  53942. aLogClass: TSQLRecordServiceNotificationsClass;
  53943. aRetryPeriodSeconds: Integer; aRemote: TSQLRestClientURI);
  53944. begin
  53945. if (self=nil) or (aRest=nil) or (aLogClass=nil) then
  53946. raise EServiceException.CreateUTF8('%.SendNotifications invalid call',[self]);
  53947. if fSendNotificationsThread<>nil then
  53948. if (aRest=fSendNotificationsRest) and (aLogClass=fSendNotificationsLogClass) then begin
  53949. fClient.InternalLog('%.SendNotifications(%,%) I% twice -> ignored',
  53950. [ClassType,aRest.ClassType,aLogClass,fInterfaceURI],sllInfo);
  53951. exit;
  53952. end else
  53953. raise EServiceException.CreateUTF8('%.SendNotifications twice',[self]);
  53954. StoreNotifications(aRest,aLogClass);
  53955. fSendNotificationsThread :=
  53956. TServiceFactoryClientNotificationThread.Create(self,aRemote,aRetryPeriodSeconds);
  53957. end;
  53958. function TServiceFactoryClient.SendNotificationsPending: integer;
  53959. begin
  53960. if (self=nil) or (fSendNotificationsThread=nil) then
  53961. result := 0 else
  53962. result := TServiceFactoryClientNotificationThread(fSendNotificationsThread).
  53963. GetPendingCountFromDB;
  53964. end;
  53965. procedure TServiceFactoryClient.SendNotificationsWait(aTimeOutSeconds: integer);
  53966. var timeOut: Int64;
  53967. begin
  53968. if SendNotificationsPending=0 then
  53969. exit;
  53970. {$ifdef WITHLOG}
  53971. fClient.LogClass.Enter;
  53972. {$endif}
  53973. timeOut := GetTickCount64+aTimeOutSeconds*1000;
  53974. repeat
  53975. Sleep(5);
  53976. if SendNotificationsPending=0 then
  53977. exit;
  53978. until GetTickCount64>timeOut;
  53979. end;
  53980. procedure TServiceFactoryClient.SetOptions(const aMethod: array of RawUTF8;
  53981. aOptions: TServiceMethodOptions);
  53982. var o: TServiceMethodOption;
  53983. m,i: integer;
  53984. begin
  53985. for o := low(o) to high(o) do
  53986. if (o in aOptions) and not (o in [optNoLogInput,optNoLogOutput]) then
  53987. raise EServiceException.CreateUTF8('%.SetOptions(%) not supported',
  53988. [self,GetEnumName(TypeInfo(TServiceMethodOption),ord(o))^]);
  53989. if high(aMethod)<0 then
  53990. for i := 0 to fInterface.fMethodsCount-1 do
  53991. fExecution[i].Options := aOptions else
  53992. for m := 0 to high(aMethod) do
  53993. fExecution[fInterface.CheckMethodIndex(aMethod[m])].Options := aOptions;
  53994. end;
  53995. function ObjectFromInterface(const aValue: IInterface): TObject;
  53996. {$ifndef HASINTERFACEASTOBJECT}
  53997. type
  53998. TObjectFromInterfaceStub = packed record
  53999. Stub: cardinal;
  54000. case integer of
  54001. 0: (ShortJmp: shortint);
  54002. 1: (LongJmp: longint)
  54003. end;
  54004. PObjectFromInterfaceStub = ^TObjectFromInterfaceStub;
  54005. {$endif}
  54006. begin
  54007. if aValue<>nil then
  54008. {$ifdef HASINTERFACEASTOBJECT}
  54009. result := aValue as TObject else // slower but always working
  54010. {$else}
  54011. with PObjectFromInterfaceStub(PPointer(PPointer(aValue)^)^)^ do
  54012. case Stub of // address of VMT[0] entry, i.e. QueryInterface
  54013. $04244483: begin
  54014. result := pointer(PtrInt(aValue)+ShortJmp);
  54015. exit;
  54016. end;
  54017. $04244481: begin
  54018. result := pointer(PtrInt(aValue)+LongJmp);
  54019. exit;
  54020. end;
  54021. else // recognize TInterfaceFactory.CreateFakeInstance() stub/mock
  54022. if Stub=PCardinal(@TInterfacedObjectFake.FakeQueryInterface)^ then begin
  54023. result := TInterfacedObjectFake(pointer(aValue)).SelfFromInterface;
  54024. exit;
  54025. end else begin
  54026. result := nil;
  54027. exit;
  54028. end;
  54029. end else
  54030. {$endif}
  54031. result := nil;
  54032. end;
  54033. function ObjectFromInterfaceImplements(const aValue: IInterface;
  54034. const aInterface: TGUID): boolean;
  54035. var obj: TObject;
  54036. begin
  54037. obj := ObjectFromInterface(aValue);
  54038. if obj=nil then
  54039. result := false else
  54040. result := obj.GetInterfaceEntry(aInterface)<>nil;
  54041. end;
  54042. procedure SetWeak(aInterfaceField: PIInterface; const aValue: IInterface);
  54043. begin
  54044. PPointer(aInterfaceField)^ := Pointer(aValue);
  54045. end;
  54046. type
  54047. TSetWeakZeroInstance = class(TObjectListHashed)
  54048. protected
  54049. fInstance: TObject;
  54050. public
  54051. constructor Create(aObject: TObject; aReference: pointer);
  54052. destructor Destroy; override;
  54053. property Instance: TObject read fInstance;
  54054. end;
  54055. TSetWeakZeroClass = class(TObjectListPropertyHashed)
  54056. protected
  54057. fHookedFreeInstance: PtrUInt;
  54058. fLock: TRTLCriticalSection;
  54059. procedure HookedFreeInstance;
  54060. public
  54061. constructor Create(aClass: TClass);
  54062. destructor Destroy; override;
  54063. function Find(aObject: TObject): TSetWeakZeroInstance;
  54064. function FindOrAdd(aObject: TObject; aReference: pointer): TSetWeakZeroInstance;
  54065. end;
  54066. { TSetWeakZeroInstance }
  54067. constructor TSetWeakZeroInstance.Create(aObject: TObject; aReference: pointer);
  54068. var wasAdded: boolean;
  54069. begin
  54070. inherited Create(false);
  54071. fInstance := aObject;
  54072. Add(aReference,wasAdded);
  54073. //assert(IndexOf(aReference)>=0);
  54074. end;
  54075. destructor TSetWeakZeroInstance.Destroy;
  54076. var i: integer;
  54077. begin
  54078. for i := 0 to Count-1 do
  54079. PPointer(List[i])^ := nil;
  54080. inherited;
  54081. end;
  54082. { TSetWeakZeroClass }
  54083. function WeakZeroClassSubProp(aObject: TObject): TObject;
  54084. begin
  54085. result := TSetWeakZeroInstance(aObject).fInstance;
  54086. end;
  54087. constructor TSetWeakZeroClass.Create(aClass: TClass);
  54088. var PVMT: ^TObject;
  54089. P: PPtrUInt;
  54090. begin
  54091. inherited Create(@WeakZeroClassSubProp);
  54092. PVMT := pointer(PtrInt(aClass)+vmtAutoTable);
  54093. if PVMT^=nil then begin
  54094. PatchCodePtrUInt(pointer(PVMT),PtrUInt(self),true); // LeaveUnprotected=true
  54095. GarbageCollectorFreeAndNil(PVMT^,self); // set to nil at finalization
  54096. end else
  54097. if TClass(PPointer(PVMT^)^)=TSQLRecordProperties then
  54098. GarbageCollectorFreeAndNil( // set to nil at finalization
  54099. TSQLRecordProperties(PVMT^).fWeakZeroClass,self) else
  54100. raise EORMException.CreateUTF8(
  54101. '%.Create: %.AutoTable VMT entry already used',[self,aClass]);
  54102. InitializeCriticalSection(fLock);
  54103. EnterCriticalSection(fLock);
  54104. {$WARN SYMBOL_DEPRECATED OFF}
  54105. P := pointer(PtrInt(aClass)+vmtFreeInstance);
  54106. {$WARN SYMBOL_DEPRECATED ON}
  54107. fHookedFreeInstance := P^;
  54108. PatchCodePtrUInt(P,PtrUInt(@TSetWeakZeroClass.HookedFreeInstance));
  54109. end;
  54110. destructor TSetWeakZeroClass.Destroy;
  54111. begin
  54112. DeleteCriticalSection(fLock);
  54113. inherited;
  54114. end;
  54115. function EnterWeakZeroClass(aObject: TObject; CreateIfNonExisting: boolean): TSetWeakZeroClass;
  54116. {$ifdef HASINLINE}inline;{$endif}
  54117. begin
  54118. result := PPointer(PPtrInt(aObject)^+vmtAutoTable)^;
  54119. if (result<>nil) and (TClass(PPointer(result)^)=TSQLRecordProperties) then
  54120. result := TSetWeakZeroClass(TSQLRecordProperties(result).fWeakZeroClass);
  54121. if result<>nil then
  54122. EnterCriticalSection(result.fLock) else
  54123. if CreateIfNonExisting then
  54124. result := TSetWeakZeroClass.Create(PPointer(aObject)^);
  54125. end;
  54126. procedure TSetWeakZeroClass.HookedFreeInstance;
  54127. begin
  54128. with EnterWeakZeroClass(self,false) do begin // if hooked -> never nil
  54129. try
  54130. Delete(self);
  54131. finally
  54132. LeaveCriticalSection(fLock);
  54133. end;
  54134. TSimpleMethodCall(fHookedFreeInstance)(self);
  54135. end;
  54136. end;
  54137. function TSetWeakZeroClass.Find(aObject: TObject): TSetWeakZeroInstance;
  54138. var i: integer;
  54139. begin
  54140. i := IndexOf(aObject); // search List[i].fInstance=aObject
  54141. if i>=0 then
  54142. result := TSetWeakZeroInstance(List[i]) else
  54143. result := nil;
  54144. end;
  54145. function TSetWeakZeroClass.FindOrAdd(aObject: TObject; aReference: pointer): TSetWeakZeroInstance;
  54146. var wasAdded: boolean;
  54147. i: integer;
  54148. begin
  54149. i := inherited Add(aObject,wasAdded);
  54150. if wasAdded then begin
  54151. result := TSetWeakZeroInstance.Create(aObject,aReference);
  54152. List[i] := result;
  54153. //assert(IndexOf(aObject)>=0);
  54154. end else begin
  54155. result := TSetWeakZeroInstance(List[i]);
  54156. result.Add(aReference,wasAdded);
  54157. end;
  54158. //assert(result.IndexOf(aReference)>=0);
  54159. //assert(result.fInstance=aObject);
  54160. end;
  54161. procedure SetWeakZero(aObject: TObject; aObjectInterfaceField: PIInterface;
  54162. const aValue: IInterface);
  54163. var aObjectWeakClass, aObjectInterfaceWeakClass: TSetWeakZeroClass;
  54164. aObjectInterfaceObject, aValueObject: TObject;
  54165. begin
  54166. if (aObjectInterfaceField=nil) or (aObject=nil) or (aObjectInterfaceField^=aValue) then
  54167. exit;
  54168. aObjectWeakClass := EnterWeakZeroClass(aObject,false);
  54169. try
  54170. if aObjectInterfaceField^<>nil then begin
  54171. if aValue=nil then
  54172. aObjectWeakClass.Delete(TObject(aObjectInterfaceField));
  54173. aObjectInterfaceObject := ObjectFromInterface(aObjectInterfaceField^);
  54174. if aObjectInterfaceObject<>nil then begin
  54175. aObjectInterfaceWeakClass := EnterWeakZeroClass(aObjectInterfaceObject,false);
  54176. if aObjectInterfaceWeakClass<>nil then
  54177. try
  54178. aObjectInterfaceWeakClass.Find(aObjectInterfaceObject).Delete(TObject(aObjectInterfaceField));
  54179. finally
  54180. LeaveCriticalSection(aObjectInterfaceWeakClass.fLock);
  54181. end;
  54182. end;
  54183. SetWeak(aObjectInterfaceField,nil);
  54184. if aValue=nil then
  54185. exit;
  54186. end;
  54187. if aObjectWeakClass=nil then // for faster Delete() just above
  54188. aObjectWeakClass := TSetWeakZeroClass.Create(PPointer(aObject)^);
  54189. aObjectWeakClass.FindOrAdd(aObject,aObjectInterfaceField);
  54190. aValueObject := ObjectFromInterface(aValue);
  54191. if aValueObject<>nil then
  54192. with EnterWeakZeroClass(aValueObject,true) do
  54193. try
  54194. FindOrAdd(aValueObject,aObjectInterfaceField);
  54195. finally
  54196. LeaveCriticalSection(fLock);
  54197. end;
  54198. SetWeak(aObjectInterfaceField,aValue);
  54199. finally
  54200. if aObjectWeakClass<>nil then
  54201. LeaveCriticalSection(aObjectWeakClass.fLock);
  54202. end;
  54203. end;
  54204. {$ifdef ISDELPHIXE}
  54205. procedure TWeakZeroInterfaceHelper.SetWeak0(aObjectInterfaceField: PIInterface;
  54206. const aValue: IInterface);
  54207. begin
  54208. SetWeakZero(self,aObjectInterfaceField,aValue);
  54209. end;
  54210. {$endif}
  54211. function ObjArraySearch(const aSQLRecordObjArray; aID: TID): TSQLRecord;
  54212. var i: integer;
  54213. a: TSQLRecordObjArray absolute aSQLRecordObjArray;
  54214. begin
  54215. for i := 0 to length(a)-1 do
  54216. if a[i].IDValue=aID then begin
  54217. result := a[i];
  54218. exit;
  54219. end;
  54220. result := nil;
  54221. end;
  54222. procedure ObjArrayRecordIDs(const aSQLRecordObjArray; out result: TInt64DynArray);
  54223. var
  54224. i, n: integer;
  54225. a: TSQLRecordObjArray absolute aSQLRecordObjArray;
  54226. begin
  54227. n := length(a);
  54228. SetLength(result,n);
  54229. for i := 0 to n-1 do
  54230. result[i] := a[i].IDValue;
  54231. end;
  54232. procedure InterfaceArrayDeleteAfterException(var aInterfaceArray;
  54233. const aItemIndex: integer; aLog: TSynLogFamily; const aLogMsg: RawUTF8;
  54234. aInstance: TObject);
  54235. begin
  54236. try
  54237. {$ifdef WITHLOG}
  54238. aLog.SynLog.Log(sllWarning,'InterfaceArrayDeleteAfterException %',[aLogMsg],aInstance);
  54239. {$endif}
  54240. InterfaceArrayDelete(aInterfaceArray,aItemIndex);
  54241. except
  54242. on E: Exception do
  54243. aLog.SynLog.Log(sllDebug,'Callback unstability at deletion: %',[E],aInstance);
  54244. end;
  54245. end;
  54246. procedure SetThreadNameWithLog(ThreadID: TThreadID; const Name: RawUTF8);
  54247. begin
  54248. {$ifdef WITHLOG}
  54249. if (SetThreadNameLog<>nil) and (ThreadID=GetCurrentThreadId) then
  54250. SetThreadNameLog.Add.LogThreadName(Name);
  54251. {$endif}
  54252. SetThreadNameDefault(ThreadID,Name);
  54253. end;
  54254. initialization
  54255. pointer(@SQLFieldTypeComp[sftAnsiText]) := @AnsiIComp;
  54256. pointer(@SQLFieldTypeComp[sftUTF8Custom]) := @AnsiIComp;
  54257. pointer(@SQLFieldTypeComp[sftObject]) := @StrComp;
  54258. {$ifndef NOVARIANTS}
  54259. pointer(@SQLFieldTypeComp[sftVariant]) := @StrComp;
  54260. pointer(@SQLFieldTypeComp[sftNullable]) := @StrComp;
  54261. {$endif}
  54262. {$ifndef USENORMTOUPPER}
  54263. pointer(@SQLFieldTypeComp[sftUTF8Text]) := @AnsiIComp;
  54264. {$endif}
  54265. SetThreadNameDefault(GetCurrentThreadID,'Main Thread');
  54266. SetThreadNameInternal := SetThreadNameWithLog;
  54267. TTextWriter.SetDefaultJSONClass(TJSONSerializer);
  54268. TJSONSerializer.RegisterObjArrayForJSON(
  54269. [TypeInfo(TSQLModelRecordPropertiesObjArray),TSQLModelRecordProperties]);
  54270. TJSONSerializer.RegisterCustomJSONSerializerFromText(
  54271. [TypeInfo(TServicesPublishedInterfaces),_TServicesPublishedInterfaces,
  54272. TypeInfo(TSQLRestServerURI),_TSQLRestServerURI]);
  54273. SynCommons.DynArrayIsObjArray := InternalIsObjArray;
  54274. InitializeCriticalSection(GlobalInterfaceResolutionLock);
  54275. TInterfaceResolverInjected.RegisterGlobal(TypeInfo(IAutoLocker),TAutoLocker);
  54276. TInterfaceResolverInjected.RegisterGlobal(TypeInfo(ILockedDocVariant),TLockedDocVariant);
  54277. assert(sizeof(TServiceMethod)and 3=0,'wrong padding');
  54278. TSQLRestServerFullMemory.RegisterClassNameForDefinition;
  54279. {$ifdef MSWINDOWS}
  54280. TSQLRestClientURINamedPipe.RegisterClassNameForDefinition;
  54281. TSQLRestClientURIMessage.RegisterClassNameForDefinition;
  54282. {$endif}
  54283. finalization
  54284. FinalizeGlobalInterfaceResolution;
  54285. end.