PrG_Creating_example_data.xml 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE sect1 PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
  3. "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
  4. <sect1 id="Creating_Example_Data">
  5. <title>Creating Example Data</title>
  6. <sect2 id="Getting_Code_Files">
  7. <title>Getting Code Files</title>
  8. <para>All the example code for the <emphasis>Programmer's Guide</emphasis>
  9. is available for download from the HPCC Systems website from the same page
  10. that the PDF is available (<ulink
  11. url="https://hpccsystems.com/training/documentation/learning-ecl">https://hpccsystems.com/training/documentation/learning-ecl</ulink>).
  12. To make this code available for use in the ECL IDE, you simply:</para>
  13. <orderedlist>
  14. <listitem>
  15. <para>Download the ECL_Code_Files.ZIP file.</para>
  16. </listitem>
  17. <listitem>
  18. <para>In the ECL IDE, highlight your "My Files" folder, right-click
  19. and select "Insert Folder" from the popup menu.</para>
  20. </listitem>
  21. <listitem>
  22. <para>Name your new folder "ProgrammersGuide" (please note -- spaces
  23. are NOT allowed in your ECL repository folder names).</para>
  24. </listitem>
  25. <listitem>
  26. <para>In the ECL IDE, highlight your "ProgrammersGuide" folder,
  27. right-click and select "Locate File in Explorer" from the popup
  28. menu.</para>
  29. </listitem>
  30. <listitem>
  31. <para>Extract all the files from the ECL_Code_Files.ZIP file into your
  32. new folder.</para>
  33. </listitem>
  34. </orderedlist>
  35. </sect2>
  36. <sect2 id="Generating_Files">
  37. <title>Generating Files</title>
  38. <para>The code that generates the example data used by all the
  39. <emphasis>Programmer's Guide</emphasis> articles is contained in a file
  40. named Gendata.ECL. You simply need to open that file in the ECL IDE
  41. (select <emphasis role="bold">File &gt; Open</emphasis> from the menu,
  42. select the Gendata.ECL file, and it will open in a Builder window) then
  43. press the <emphasis role="bold">Submit</emphasis> button to generate the
  44. data files. The process takes a couple of minutes to run. Here is the
  45. code, fully explained.</para>
  46. </sect2>
  47. <sect2 id="Some_Constants">
  48. <title>Some Constants</title>
  49. <programlisting>IMPORT std;
  50. P_Mult1 := 1000;
  51. P_Mult2 := 1000;
  52. TotalParents := P_Mult1 * P_Mult2;
  53. TotalChildren := 5000000;</programlisting>
  54. <para>These constants define the numbers used to generate 1,000,000 parent
  55. records and 5,000,000 child records. By defining these once as attributes
  56. the code could be easily made to generate a smaller number of parent
  57. records (such as 10,000 by changing both multipliers from 1000 to 100).
  58. However, the code as written is designed for a maximum of 1,000,000 parent
  59. records and would have to be changed in several places to accommodate
  60. generating more. The number of child records can be changed either
  61. direction without any other changes to the code (although if pushed too
  62. far upward you may encounter runtime errors regarding the maximum variable
  63. record length for the nested child dataset). For the purposes of
  64. demonstrating the techniques in these <emphasis>Programmer's
  65. Guide</emphasis> articles, 1,000,000 parent and 5,000,000 child records
  66. are more than sufficient.</para>
  67. </sect2>
  68. <sect2 id="The_RECORD_Structures">
  69. <title>The RECORD Structures</title>
  70. <programlisting>Layout_Person := RECORD
  71. UNSIGNED3 PersonID;
  72. STRING15 FirstName;
  73. STRING25 LastName;
  74. STRING1 MiddleInitial;
  75. STRING1 Gender;
  76. STRING42 Street;
  77. STRING20 City;
  78. STRING2 State;
  79. STRING5 Zip;
  80. END;
  81. Layout_Accounts := RECORD
  82. STRING20 Account;
  83. STRING8 OpenDate;
  84. STRING2 IndustryCode;
  85. STRING1 AcctType;
  86. STRING1 AcctRate;
  87. UNSIGNED1 Code1;
  88. UNSIGNED1 Code2;
  89. UNSIGNED4 HighCredit;
  90. UNSIGNED4 Balance;
  91. END;
  92. Layout_Accounts_Link := RECORD
  93. UNSIGNED3 PersonID;
  94. Layout_Accounts;
  95. END;
  96. Layout_Combined := RECORD,MAXLENGTH(1000)
  97. Layout_Person;
  98. DATASET(Layout_Accounts) Accounts;
  99. END;
  100. </programlisting>
  101. <para>These RECORD structures define the field layouts for three datasets:
  102. a parent file (Layout_Person), a child file (Layout_Accounts_Link), and a
  103. parent with nested child dataset (Layout_Combined). These are used to
  104. generate three separate data files. The Layout_Accounts_Link and
  105. Layout_Accounts structures are separate because the child records in the
  106. nested structure will not contain the linking field to the parent, whereas
  107. the separate child file must contain the link.</para>
  108. </sect2>
  109. <sect2 id="Starting_Point_Data">
  110. <title>Starting Point Data</title>
  111. <programlisting>//define data for record generation:
  112. //100 possible middle initials, 52 letters and 48 blanks
  113. SetMiddleInitials := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ' +
  114. 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ';
  115. //1000 First names
  116. SET OF STRING14 SetFnames := [
  117. 'TIMTOHY ','ALCIAN ','CHAMENE ',
  118. ... ];
  119. //1000 Last names
  120. SET OF STRING16 SetLnames := [
  121. 'BIALES ','COOLING ','CROTHALL ',
  122. ... ];
  123. </programlisting>
  124. <para>These sets define the data that will be used to generate the
  125. records. By providing 1,000 first and last names, this code can generate
  126. 1,000,000 unique names.</para>
  127. <para><programlisting> //2400 street addresses to choose from
  128. SET OF STRING31 SetStreets := [
  129. '1 SANDHURST DR ','1 SPENCER LN ',
  130. ... ];
  131. //Matched sets of 9540 City,State, Zips
  132. SET OF STRING15 SetCity := [
  133. 'ABBEVILLE ','ABBOTTSTOWN ','ABELL ',
  134. ... ];
  135. SET OF STRING2 SetStates := [
  136. 'LA','PA','MD','NC','MD','TX','TX','IL','MA','LA','WI','NJ',
  137. ... ];
  138. SET OF STRING5 SetZips := [
  139. '70510','17301','20606','28315','21005','79311','79604',
  140. ... ];
  141. </programlisting>Having 2400 street addresses and 9540 (valid) city, state,
  142. zip combinations provides plenty of opportunity to generate a reasonable
  143. mix of addresses.</para>
  144. </sect2>
  145. <sect2 id="Generating_Parent_Records">
  146. <title>Generating Parent Records</title>
  147. <programlisting>BlankSet := DATASET([{0,'','','','','','','','',[]}],
  148. Layout_Combined);
  149. CountCSZ := 9540;</programlisting>
  150. <para>Here is the beginning of the data generation code. The BlankSet is a
  151. single empty “seed” record, used to start the process. The CountCSZ
  152. attribute simply defines the maximum number of city, state, zip
  153. combinations that are available for use in subsequent calculations that
  154. will determine which to use in a given record.</para>
  155. <programlisting>Layout_Combined CreateRecs(Layout_Combined L,
  156. INTEGER C,
  157. INTEGER W) := TRANSFORM
  158. SELF.FirstName := IF(W=1,SetFnames[C],L.FirstName);
  159. SELF.LastName := IF(W=2,SetLnames[C],L.LastName);
  160. SELF := L;
  161. END;
  162. base_fn := NORMALIZE(BlankSet,P_Mult1,CreateRecs(LEFT,COUNTER,1));
  163. base_fln := NORMALIZE(base_fn ,P_Mult2,CreateRecs(LEFT,COUNTER,2));
  164. </programlisting>
  165. <para>The purpose of this code is to generate 1,000,000 unique first/last
  166. name records as a starting point. The NORMALIZE operation is unique in
  167. that its second parameter defines the number of times to call the
  168. TRANSFORM function for each input record. This makes it uniquely suited to
  169. generating the kind of “bogus” data we need.</para>
  170. <para>We're doing two NORMALIZE operations here. The first generates 1,000
  171. records with unique first names from the single blank record in the
  172. BlankSet inline DATASET. Then the second takes the 1,000 records from the
  173. first NORMALIZE and creates 1,000 new records with unique last names for
  174. each input record, resulting in 1,000,000 unique first/last name
  175. records.</para>
  176. <para>One interesting “trick” here is the use of a single TRANSFORM
  177. function for both of the NORMALIZE operations. Defining the TRANSFORM to
  178. receive one “extra” (third) parameter than it normally takes is what
  179. allows this. This parameter simply flags which NORMALIZE pass the
  180. TRANSFORM is doing.</para>
  181. <programlisting>Layout_Combined PopulateRecs(Layout_Combined L,
  182. Layout_Combined R,
  183. INTEGER HashVal) := TRANSFORM
  184. CSZ_Rec := (HashVal % CountCSZ) + 1;
  185. SELF.PersonID := IF(L.PersonID = 0,
  186. Thorlib.Node() + 1,
  187. L.PersonID + CLUSTERSIZE);
  188. SELF.MiddleInitial := SetMiddleInitials[(HashVal % 100) + 1 ];
  189. SELF.Gender := CHOOSE((HashVal % 2) + 1,'F','M');
  190. SELF.Street := SetStreets[(HashVal % 2400) + 1 ];
  191. SELF.City := SetCity[CSZ_Rec];
  192. SELF.State := SetStates[CSZ_Rec];
  193. SELF.Zip := SetZips[CSZ_Rec];
  194. SELF := R;
  195. END;
  196. base_fln_dist := DISTRIBUTE(base_fln,HASH32(FirstName,LastName));
  197. base_people := ITERATE(base_fln_dist,
  198. PopulateRecs(LEFT,
  199. RIGHT,
  200. HASHCRC(RIGHT.FirstName,RIGHT.LastName)),
  201. LOCAL);
  202. base_people_dist := DISTRIBUTE(base_people,HASH32(PersonID));
  203. </programlisting>
  204. <para>Once the two NORMALIZE operations have done their work, the next
  205. task is to populate the rest of the fields. Since one of those fields is
  206. the PersonID, which is the unique identifier field for the record, the
  207. fastest way to populate it is with ITERATE using the LOCAL option. Using
  208. the Thorlib.Node() function and CLUSTERSIZE compiler directive, you can
  209. uniquely number each record in parallel on each node with ITERATE. You may
  210. end up with a few holes in the numbering towards the end, but since the
  211. only requirement here is uniqueness and not contiguity, those holes are
  212. irrelevant. Since the first two NORMALIZE operations took place on a
  213. single node (look at the data skews shown in the ECL Watch graph), the
  214. first thing to do is DISTRIBUTE the records so each node has a
  215. proportional chunk of the data to work with. Then the ITERATE can do its
  216. work on each chunk of records in parallel.</para>
  217. <para>To introduce an element of randomity to the data choices, the
  218. ITERATE passes a hash value to the TRANSFORM function as an “extra” third
  219. parameter. This is the same technique used previously, but passing
  220. calculated values instead of constants.</para>
  221. <para>The CSZ_Rec attribute definition illustrates the use of local
  222. attribute definitions inside TRANSFORM functions. Defining the expression
  223. once, then using it multiple times as needed to produce a valid city,
  224. state, zip combination. The rest of the fields are populated by data
  225. selected using the passed in hash value in their expressions. The modulus
  226. division operator (%—produces the remainder of the division) is used to
  227. ensure that a value is calculated that is in the valid range of the number
  228. of elements for the given set of data from which the field is
  229. populated.</para>
  230. </sect2>
  231. <sect2 id="Generating_Child_Records">
  232. <title>Generating Child Records</title>
  233. <programlisting>BlankKids := DATASET([{0,'','','','','',0,0,0,0}],
  234. Layout_Accounts_Link);
  235. SetLinks := SET(base_people,PersonID);
  236. SetIndustryCodes := ['BB','DC','ON','FM','FP','FF','FC','FA','FZ',
  237. 'CG','FS','OC','ZZ','HZ','UT','HF','CS','DM',
  238. 'JA','FY','HT','UE','DZ','AT'];
  239. SetAcctRates := ['1','0','9','*','Z','5','B','2',
  240. '3','4','A','7','8','E','C'];
  241. SetDateYears := ['1987','1988','1989','1990','1991','1992','1993',
  242. '1994','1995','1996','1997','1998','1999','2000',
  243. '2001','2002','2003','2004','2005','2006'];
  244. SetMonthDays := [31,28,31,30,31,30,31,31,30,31,30,31];
  245. SetNarrs := [229,158,2,0,66,233,123,214,169,248,67,127,168,
  246. 65,208,114,73,218,238,57,125,113,88,
  247. 247,244,121,54,220,98,97];
  248. </programlisting>
  249. <para>Once again, we start by defining a “seed” record for the process as
  250. an inline DATASET and several sets of appropriate data for the specific
  251. fields. The SET function builds a set of valid PersonID values to use to
  252. create the links between the parent and child records.</para>
  253. <programlisting>Layout_Accounts_Link CreateKids(Layout_Accounts_Link L,
  254. INTEGER C) := TRANSFORM
  255. CSZ_IDX := C % CountCSZ + 1;
  256. HashVal := HASH32(SetCity[CSZ_IDX],SetStates[CSZ_IDX],SetZips[CSZ_IDX]);
  257. DateMonth := HashVal % 12 + 1;
  258. SELF.PersonID := CHOOSE(TRUNCATE(C / TotalParents ) + 1,
  259. IF(C % 2 = 0,
  260. SetLinks[C % TotalParents + 1],
  261. SetLinks[TotalParents - (C % TotalParents )]),
  262. IF(C % 3 &lt;&gt; 0,
  263. SetLinks[C % TotalParents + 1],
  264. SetLinks[TotalParents - (C % TotalParents )]),
  265. IF(C % 5 = 0,
  266. SetLinks[C % TotalParents + 1],
  267. SetLinks[TotalParents - (C % TotalParents )]),
  268. IF(C % 7 &lt;&gt; 0,
  269. SetLinks[C % TotalParents + 1],
  270. SetLinks[TotalParents - (C % TotalParents )]),
  271. SetLinks[C % TotalParents + 1]);
  272. SELF.Account := (STRING)HashVal;
  273. SELF.OpenDate := SetDateYears[DateMonth] + INTFORMAT(DateMonth,2,1) +
  274. INTFORMAT(HashVal % SetMonthDays[DateMonth]+1,2,1);
  275. SELF.IndustryCode := SetIndustrycodes[HashVal % 24 + 1];
  276. SELF.AcctType := CHOOSE(HashVal%5+1,'O','R','I','9',' ');
  277. SELF.AcctRate := SetAcctRates[HashVal % 15 + 1];
  278. SELF.Code1 := SetNarrs[HashVal % 15 + 1];
  279. SELF.Code2 := SetNarrs[HashVal % 15 + 16];
  280. SELF.HighCredit := HashVal % 50000;
  281. SELF.Balance := TRUNCATE((HashVal % 50000) * ((HashVal % 100 + 1) / 100));
  282. END;
  283. base_kids := NORMALIZE( BlankKids,
  284. TotalChildren,
  285. CreateKids(LEFT,COUNTER));
  286. base_kids_dist := DISTRIBUTE(base_kids,HASH32(PersonID));
  287. </programlisting>
  288. <para>This process is similar to the one used for the parent records. This
  289. time, instead of passing in a hash value, a local attribute does that work
  290. inside the TRANSFORM. Just as before, the hash value is used to select the
  291. actual data to go in each field of the record.</para>
  292. <para>The interesting bit here is the expression to determine the PersonID
  293. field value. Since we're generating 5,000,000 child records it would be
  294. very simple to just give each parent five children. However, real-world
  295. data rarely looks like that. Therefore, the CHOOSE function is used to
  296. select a different method for each set of a million child records. The
  297. first million uses the first IF expression, and the second million uses
  298. the second, and so on... This creates a varying number of children for
  299. each parent, ranging from one to nine.</para>
  300. </sect2>
  301. <sect2 id="Create_the_Nested_Child-Dataset_Records">
  302. <title>Create the Nested Child Dataset Records</title>
  303. <programlisting>Layout_Combined AddKids(Layout_Combined L, base_kids R) := TRANSFORM
  304. SELF.Accounts := L.Accounts +
  305. ROW({R.Account,R.OpenDate,R.IndustryCode,
  306. R.AcctType,R.AcctRate,R.Code1,
  307. R.Code2,R.HighCredit,R.Balance},
  308. Layout_Accounts);
  309. SELF := L;
  310. END;
  311. base_combined := DENORMALIZE( base_people_dist,
  312. base_kids_dist,
  313. LEFT.PersonID = RIGHT.PersonID,
  314. AddKids(LEFT, RIGHT));
  315. </programlisting>
  316. <para>Now that we have separate recordsets of parent and child records,
  317. the next step is to combine them into a single dataset with each parent's
  318. child data nested within the same physical record as the parent. The
  319. reason for nesting the child data this way is to allow easy parent-child
  320. queries in the Data Refinery and Rapid data Delivery Engine without
  321. requiring the use of separate JOIN steps to make the links between the
  322. parent and child records.</para>
  323. <para>To build the nested child dataset requires the DENORMALIZE
  324. operation. This operation finds the links between the parent records and
  325. their associated children, calling the TRANSFORM function as many times as
  326. there are child records for each parent. The interesting technique here is
  327. the use of the ROW function to construct each additional nested child
  328. record. This is done to eliminate the linking field (PersonID) from each
  329. child record stored in the combined dataset, since it is the same value as
  330. contained in the parent record's PersonID field.</para>
  331. </sect2>
  332. <sect2 id="Write_Files_to_Disk">
  333. <title>Write Files to Disk</title>
  334. <programlisting>O1 := OUTPUT(PROJECT(base_people_dist,Layout_Person),,'~PROGGUIDE::EXAMPLEDATA::People',OVERWRITE);
  335. O2 := OUTPUT(base_kids_dist,,'~PROGGUIDE::EXAMPLEDATA::Accounts',OVERWRITE);
  336. O3 := OUTPUT(base_combined,,'~PROGGUIDE::EXAMPLEDATA::PeopleAccts',OVERWRITE);
  337. P1 := PARALLEL(O1,O2,O3);
  338. </programlisting>
  339. <para>These OUTPUT attribute definitions will write the datasets to disk.
  340. They are written as attribute definitions because they will be used in a
  341. SEQUENTIAL action. The PARALLEL action attribute simply indicates that all
  342. these disk writes can occur “simultaneously” if the optimizer decides it
  343. can do that.</para>
  344. <para>The first OUTPUT uses a PROJECT to produce the parent records as a
  345. separate file because the data was originally generated into a RECORD
  346. structure that contains the nested child DATASET field (Accounts) in
  347. preparation for creating the third file. The PROJECT eliminates that empty
  348. Accounts field from the output for this dataset.</para>
  349. <programlisting>D1 := DATASET('~PROGGUIDE::EXAMPLEDATA::People',
  350. {Layout_Person,UNSIGNED8 RecPos{virtual(fileposition)}}, THOR);
  351. D2 := DATASET('~PROGGUIDE::EXAMPLEDATA::Accounts',
  352. {Layout_Accounts_Link,UNSIGNED8 RecPos{virtual(fileposition)}},THOR);
  353. D3 := DATASET('~PROGGUIDE::EXAMPLEDATA::PeopleAccts',
  354. {,MAXLENGTH(1000) Layout_Combined,UNSIGNED8 RecPos{virtual(fileposition)}},THOR);
  355. </programlisting>
  356. <para>These DATASET declarations are needed to be able to build indexes.
  357. The UNSIGNED8 RecPos fields are the virtual fields (they only exist at
  358. runtime and not on disk) that are the internal record pointers. They're
  359. declared here to be able to reference them in the subsequent INDEX
  360. declarations.</para>
  361. <programlisting>I1 := INDEX(D1,{PersonID,RecPos},'~PROGGUIDE::EXAMPLEDATA::KEYS::People.PersonID');
  362. I2 := INDEX(D2,{PersonID,RecPos},'~PROGGUIDE::EXAMPLEDATA::KEYS::Accounts.PersonID');
  363. I3 := INDEX(D3,{PersonID,RecPos},'~PROGGUIDE::EXAMPLEDATA::KEYS::PeopleAccts.PersonID');
  364. B1 := BUILD(I1,OVERWRITE);
  365. B2 := BUILD(I2,OVERWRITE);
  366. B3 := BUILD(I3,OVERWRITE);
  367. P2 := PARALLEL(B1,B2,B3);
  368. </programlisting>
  369. <para>These INDEX declarations allow the BUILD actions to use the
  370. single-parameter form. Once again, the PARALLEL action attribute indicates
  371. the index build may be done all at the same time.</para>
  372. <programlisting>SEQUENTIAL(P1,P2);</programlisting>
  373. <para>This SEQUENTIAL action simply says, “write all the data files to
  374. disk, and then build the indexes.”</para>
  375. </sect2>
  376. <sect2 id="Adding_the_Files-to_the_Repository">
  377. <title>Defining the Files</title>
  378. <para>Once the datasets and indexes have been written to disk you must
  379. declare the files in order to use them in the example ECL code in the rest
  380. of the articles. These declarations are contained in the DeclareData.ECL
  381. file. To make them available to the rest of the example code you simply
  382. need to IMPORT it. Therefore, at the beginning of each example you will
  383. find this line of code:</para>
  384. <programlisting>IMPORT $;</programlisting>
  385. <para>This IMPORTs all the files in the ProgrammersGuide folder (including
  386. the DeclareData MODULE structure definition). Referencing anything from
  387. DeclareData is done by prepending $.DeclareData to the name of the EXPORT
  388. definition you need to use, like this:</para>
  389. <programlisting>MyFile := $.DeclareData.Person.File; //rename $DeclareData.Person.File to MyFile to make
  390. //subsequent code simpler</programlisting>
  391. <para>Here is some of the code contained in the DeclareData.ECL
  392. file:</para>
  393. <programlisting>EXPORT DeclareData := MODULE
  394. EXPORT Layout_Person := RECORD
  395. UNSIGNED3 PersonID;
  396. STRING15 FirstName;
  397. STRING25 LastName;
  398. STRING1 MiddleInitial;
  399. STRING1 Gender;
  400. STRING42 Street;
  401. STRING20 City;
  402. STRING2 State;
  403. STRING5 Zip;
  404. END;
  405. EXPORT Layout_Accounts := RECORD
  406. STRING20 Account;
  407. STRING8 OpenDate;
  408. STRING2 IndustryCode;
  409. STRING1 AcctType;
  410. STRING1 AcctRate;
  411. UNSIGNED1 Code1;
  412. UNSIGNED1 Code2;
  413. UNSIGNED4 HighCredit;
  414. UNSIGNED4 Balance;
  415. END;
  416. EXPORT Layout_Accounts_Link := RECORD
  417. UNSIGNED3 PersonID;
  418. Layout_Accounts;
  419. END;
  420. SHARED Layout_Combined := RECORD,MAXLENGTH(1000)
  421. Layout_Person;
  422. DATASET(Layout_Accounts) Accounts;
  423. END;
  424. EXPORT Person := MODULE
  425. EXPORT File := DATASET('~PROGGUIDE::EXAMPLEDATA::People',Layout_Person, THOR);
  426. EXPORT FilePlus := DATASET('~PROGGUIDE::EXAMPLEDATA::People',
  427. {Layout_Person,UNSIGNED8 RecPos{virtual(fileposition)}}, THOR);
  428. END;
  429. EXPORT Accounts := DATASET('~PROGGUIDE::EXAMPLEDATA::Accounts',
  430. {Layout_Accounts_Link,
  431. UNSIGNED8 RecPos{virtual(fileposition)}},
  432. THOR);
  433. EXPORT PersonAccounts:= DATASET('~PROGGUIDE::EXAMPLEDATA::PeopleAccts',
  434. {Layout_Combined,
  435. UNSIGNED8 RecPos{virtual(fileposition)}},
  436. THOR);
  437. EXPORT IDX_Person_PersonID :=
  438. INDEX(Person,
  439. {PersonID,RecPos},
  440. '~PROGGUIDE::EXAMPLEDATA::KEYS::People.PersonID');
  441. EXPORT IDX_Accounts_PersonID :=
  442. INDEX(Accounts,
  443. {PersonID,RecPos},
  444. '~PROGGUIDE::EXAMPLEDATA::KEYS::Accounts.PersonID');
  445. EXPORT IDX_PersonAccounts_PersonID :=
  446. INDEX(PersonAccounts,
  447. {PersonID,RecPos},
  448. '~PROGGUIDE::EXAMPLEDATA::KEYS::PeopleAccts.PersonID');
  449. END;
  450. </programlisting>
  451. <para>By using a MODULE structure as a container, all the DATASET and
  452. INDEX declarations are in a single attribute editor window. This makes
  453. maintenance and update simple while allowing complete access to them
  454. all.</para>
  455. </sect2>
  456. </sect1>