transactd 3.6.1 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/bin/common/{tdclc_32_3_6.dll → tdclc_32_3_7.dll} +0 -0
  3. data/bin/common/{tdclc_64_3_6.dll → tdclc_64_3_7.dll} +0 -0
  4. data/build/swig/ruby/tdclrb_wrap.cpp +125 -17
  5. data/build/tdclc/tdclc.cbproj +1 -1
  6. data/build/tdclc/tdclc.rc +4 -4
  7. data/build/tdclcpp/tdclcpp.rc +4 -4
  8. data/build/tdclcpp/tdclcpp_bc.cbproj +1 -1
  9. data/build/tdclrb/tdclrb.rc +4 -4
  10. data/source/bzs/db/protocol/tdap/client/field.cpp +24 -0
  11. data/source/bzs/db/protocol/tdap/client/field.h +2 -0
  12. data/source/bzs/db/protocol/tdap/client/groupQuery.cpp +243 -59
  13. data/source/bzs/db/protocol/tdap/client/groupQuery.h +8 -0
  14. data/source/bzs/db/protocol/tdap/client/memRecord.cpp +19 -6
  15. data/source/bzs/db/protocol/tdap/client/memRecord.h +10 -1
  16. data/source/bzs/db/protocol/tdap/client/recordset.cpp +17 -0
  17. data/source/bzs/db/protocol/tdap/client/recordset.h +3 -0
  18. data/source/bzs/db/protocol/tdap/client/recordsetImple.h +131 -30
  19. data/source/bzs/db/protocol/tdap/client/table.cpp +11 -2
  20. data/source/bzs/db/protocol/tdap/mysql/recordsetReader.h +1 -0
  21. data/source/bzs/db/protocol/tdap/tdapcapi.h +5 -5
  22. data/source/bzs/test/tdclatl/test_v3.js +22 -0
  23. data/source/bzs/test/tdclphp/transactd_v3_Test.php +35 -0
  24. data/source/bzs/test/tdclrb/transactd_v3_spec.rb +40 -0
  25. data/source/bzs/test/trdclengn/testField.h +320 -0
  26. data/source/bzs/test/trdclengn/test_tdclcpp_v3.cpp +9 -0
  27. data/source/global/tdclatl/Recordset.cpp +44 -2
  28. data/source/global/tdclatl/Recordset.h +6 -2
  29. data/source/global/tdclatl/tdclatl.idl +5 -1
  30. metadata +4 -4
@@ -226,32 +226,111 @@ bool fieldValues::isNull(int index) const
226
226
  }
227
227
 
228
228
 
229
+ struct recordCompItem
230
+ {
231
+ judgeFunc isMatchFunc;
232
+ comp1Func compFunc;
233
+ short index;
234
+ short indexTmp; // For join match. converted field index
235
+ short cmpIndex; // For CMPLOGICAL_FIELD only
236
+ ushort_td cmpLen;
237
+ uchar_td compType;
238
+ char combine;
239
+ struct
240
+ {
241
+ bool nullable : 1;
242
+ bool nulllog : 1; // condition has isNull or isNotNull
243
+ };
244
+ };
245
+
246
+ inline char getCombine(const std::vector<std::_tstring>& tokns, int index)
247
+ {
248
+ if (index + 3 < (int)tokns.size())
249
+ {
250
+ std::_tstring s = tokns[index + 3];
251
+ if (s == _T("or"))
252
+ return eCor;
253
+ else if (s == _T("and"))
254
+ return eCand;
255
+ }
256
+ return eCend;
257
+ }
258
+
259
+ inline uchar_td getCompType(const _TCHAR* v, bool compField, bool part)
260
+ {
261
+
262
+ uchar_td compType = getFilterLogicTypeCode(v);
263
+ if (compField)
264
+ compType |= CMPLOGICAL_FIELD;
265
+ if (!part)
266
+ compType |= CMPLOGICAL_VAR_COMP_ALL;
267
+ return compType;
268
+ }
269
+
270
+ inline bool getNullLog(uchar_td compType)
271
+ {
272
+ uchar_td log = (compType & 0xf);
273
+ return ((log == eIsNull) || (log == eIsNotNull));
274
+ }
275
+
276
+ inline comp1Func getCompFunc2(const fielddef& fdd, uchar_td compType)
277
+ {
278
+ uchar_td type = fdd.type;
279
+ if (fdd.isLegacyTimeFormat())
280
+ {
281
+ if (type == ft_mytime) type = ft_mytime_num_cmp;
282
+ if (type == ft_mydatetime) type = ft_mydatetime_num_cmp;
283
+ if (type == ft_mytimestamp) type = ft_mytimestamp_num_cmp;
284
+ }
285
+ return getCompFunc(type, fdd.len, compType,
286
+ fdd.varLenBytes() + fdd.blobLenBytes());
287
+ }
288
+
289
+ inline short getIndex(const fielddefs* fdinfo, const std::_tstring& v)
290
+ {
291
+ short index = fdinfo->indexByName(v);
292
+ if (index == -1)
293
+ THROW_BZS_ERROR_WITH_MSG(_T("recordsetQuery:Invalid field name ")
294
+ + std::_tstring(v));
295
+ return index;
296
+ }
297
+
298
+ inline bool isSingedInteger(uchar_td type)
299
+ {
300
+ return ((type == ft_integer) || (type == ft_autoinc));
301
+ }
302
+
303
+ inline bool isUnsingedInteger(uchar_td type)
304
+ {
305
+ return ((type == ft_logical) || (type == ft_uinteger) || (type == ft_set) ||
306
+ (type == ft_bit) || (type == ft_enum) || (type == ft_autoIncUnsigned) ||
307
+ (type == ft_myyear));
308
+ }
309
+
310
+ bool canDirectComp(const fielddef& l, const fielddef& r)
311
+ {
312
+ if (l.type == r.type && l.charsetIndex() == r.charsetIndex())
313
+ return true;
314
+ if (isSingedInteger(l.type) == isSingedInteger(r.type)) return true;
315
+ if (isUnsingedInteger(l.type) == isUnsingedInteger(r.type)) return true;
316
+ return false;
317
+ }
318
+
319
+
229
320
  // ---------------------------------------------------------------------------
230
321
  // struct recordsetQueryImple
231
322
  // ---------------------------------------------------------------------------
232
323
  struct recordsetQueryImple
233
324
  {
234
- row_ptr row;
235
- struct compItem
236
- {
237
- judgeFunc isMatchFunc;
238
- comp1Func compFunc;
239
- short index;
240
- short cmpIndex; // For CMPLOGICAL_FIELD only
241
- uchar_td compType;
242
- char combine;
243
- struct
244
- {
245
- bool nullable : 1;
246
- bool nulllog : 1;
247
- };
248
- };
249
- std::vector<compItem> compItems;
250
- fielddefs compFields;
325
+ row_ptr row; // For matchBy condition values or join target of converted fields
326
+ row_ptr joinRow; // For join target row
327
+ std::vector<recordCompItem> compItems; // condition parms
328
+ fielddefs compFields; // condition fields
251
329
  short endIndex;
330
+ short matchHint;
252
331
  bool mysqlnull;
253
332
 
254
- recordsetQueryImple() : row(NULL),mysqlnull(false) {}
333
+ recordsetQueryImple() : row(NULL), joinRow(NULL), mysqlnull(false) {}
255
334
  recordsetQueryImple(const recordsetQueryImple& r)
256
335
  : row(r.row), compItems(r.compItems), compFields(r.compFields), mysqlnull(r.mysqlnull)
257
336
  {
@@ -304,31 +383,39 @@ recordsetQuery::~recordsetQuery()
304
383
  delete m_imple;
305
384
  }
306
385
 
386
+ void recordsetQuery::createTempRecord()
387
+ {
388
+ m_imple->compFields.calcFieldPos(0 /*startIndex*/, true);
389
+ m_imple->row = memoryRecord::create(m_imple->compFields);
390
+ m_imple->row->addref();
391
+ m_imple->row->setRecordData(autoMemory::create(), 0, 0, &m_imple->endIndex, true);
392
+ }
393
+ /*
394
+ init for matchBy
395
+ */
307
396
  void recordsetQuery::init(const fielddefs* fdinfo)
308
397
  {
309
398
  m_imple->mysqlnull = fdinfo->mysqlnullEnable();
310
399
  const std::vector<std::_tstring>& tokns = getWheres();
311
- m_imple->compFields.clear();
400
+ m_imple->row = NULL;
312
401
  m_imple->compItems.clear();
402
+ m_imple->compFields.clear();
313
403
  for (int i = 0; i < (int)tokns.size(); i += 4)
314
404
  {
315
- recordsetQueryImple::compItem itm;
316
- itm.index = fdinfo->indexByName(tokns[i].c_str());
317
- if (itm.index == -1)
318
- THROW_BZS_ERROR_WITH_MSG(_T("recordsetQuery:Invalid field name ") + tokns[i]);
319
- itm.nullable = (*fdinfo)[itm.index].isNullable();
405
+ recordCompItem itm;
406
+ itm.indexTmp = -1;
407
+ itm.index = getIndex(fdinfo, tokns[i]);
408
+ itm.nullable = (*fdinfo)[itm.index].isNullable() & m_imple->mysqlnull;
409
+ itm.cmpLen = (*fdinfo)[itm.index].len;
320
410
  m_imple->compItems.push_back(itm);
321
411
  m_imple->compFields.push_back(&((*fdinfo)[itm.index]));
322
412
  }
323
- m_imple->compFields.calcFieldPos(0 /*startIndex*/, true);
324
- m_imple->row = memoryRecord::create(m_imple->compFields);
325
- m_imple->row->addref();
326
- m_imple->row->setRecordData(autoMemory::create(), 0, 0, &m_imple->endIndex, true);
413
+ createTempRecord();
327
414
 
328
415
  int index = 0;
329
416
  for (int i = 0; i < (int)tokns.size(); i += 4)
330
417
  {
331
- recordsetQueryImple::compItem& itm = m_imple->compItems[index];
418
+ recordCompItem& itm = m_imple->compItems[index];
332
419
  field fd = (*m_imple->row)[index];
333
420
 
334
421
  std::_tstring value = tokns[i + 2];
@@ -337,66 +424,163 @@ void recordsetQuery::init(const fielddefs* fdinfo)
337
424
  {
338
425
  value.erase(value.begin());
339
426
  value.erase(value.end() - 1);
340
- itm.cmpIndex = fdinfo->indexByName(value);
341
- if (itm.cmpIndex == -1)
342
- THROW_BZS_ERROR_WITH_MSG(_T("recordsetQuery:Invalid field name ") + value);
427
+ itm.cmpIndex = getIndex(fdinfo, value);
343
428
  }
344
429
  else
345
430
  fd = value.c_str();
346
431
  bool part = fd.isCompPartAndMakeValue();
347
- itm.compType = getFilterLogicTypeCode(tokns[i + 1].c_str());
348
- if (compField)
349
- itm.compType |= CMPLOGICAL_FIELD;
350
- if (!part)
351
- itm.compType |= CMPLOGICAL_VAR_COMP_ALL;
352
- eCompType log = (eCompType)(itm.compType & 0xf);
353
- itm.nulllog = ((log == eIsNull) || (log == eIsNotNull));
432
+ itm.compType = getCompType(tokns[i + 1].c_str(), compField, part);
433
+ itm.nulllog = (m_imple->mysqlnull && getNullLog(itm.compType));
354
434
  fielddef& fdd = const_cast<fielddef&>(m_imple->compFields[index]);
355
435
  fdd.len = m_imple->compFields[index].compDataLen((const uchar_td*)fd.ptr(), part);
356
- uchar_td type = fdd.type;
357
- if (fdd.isLegacyTimeFormat())
358
- {
359
- if (type == ft_mytime) type = ft_mytime_num_cmp;
360
- if (type == ft_mydatetime) type = ft_mydatetime_num_cmp;
361
- if (type == ft_mytimestamp) type = ft_mytimestamp_num_cmp;
436
+ itm.compFunc = getCompFunc2(fdd, itm.compType);
437
+ itm.isMatchFunc = getJudgeFunc((eCompType)itm.compType);
438
+ itm.combine = getCombine(tokns, i);
439
+ itm.cmpLen = fdd.len;
440
+ if (itm.compType & CMPLOGICAL_FIELD)
441
+ { // No field type convert
442
+ const fielddef& fdr = m_imple->compFields[itm.cmpIndex];
443
+ itm.cmpLen = std::min(fdd.len, fdr.len);
362
444
  }
445
+ ++index;
446
+ }
447
+ }
363
448
 
364
- itm.compFunc = getCompFunc(type, fdd.len, itm.compType,
365
- fdd.varLenBytes() + fdd.blobLenBytes());
449
+ /*
450
+ init for join
451
+ */
452
+ void recordsetQuery::init(const fielddefs* fdinfo, const fielddefs* rfdinfo)
453
+ {
454
+ m_imple->mysqlnull = fdinfo->mysqlnullEnable();
455
+ const std::vector<std::_tstring>& tokns = getWheres();
456
+ m_imple->row = NULL;
457
+ m_imple->compItems.clear();
458
+ m_imple->compFields.clear();
459
+ for (int i = 0; i < (int)tokns.size(); i += 4)
460
+ {
461
+ recordCompItem itm;
462
+ itm.indexTmp = -1;
463
+ itm.index = getIndex(fdinfo, tokns[i]);
464
+ itm.cmpIndex = getIndex(rfdinfo, tokns[i+2]);
465
+ itm.nullable = true;//(*fdinfo)[itm.index].isNullable() | (*rfdinfo)[itm.cmpIndex].isNullable();
466
+ itm.compType = getCompType(tokns[i + 1].c_str(), true, false);
467
+ itm.nulllog = getNullLog(itm.compType);
468
+ if (itm.nulllog)
469
+ THROW_BZS_ERROR_WITH_MSG(_T("recordsetQuery:Join can not use null compare. "));
470
+
366
471
  itm.isMatchFunc = getJudgeFunc((eCompType)itm.compType);
472
+ itm.combine = getCombine(tokns, i);
473
+
474
+ // different field type
475
+ const fielddef& fdd = (*fdinfo)[itm.index];
476
+ const fielddef& fddr = (*rfdinfo)[itm.cmpIndex];
477
+ if (fdd.isBlob() || fddr.isBlob())
478
+ THROW_BZS_ERROR_WITH_MSG(_T("recordsetQuery: BLOB or TEXT can not use for join key field. "));
479
+ if (!canDirectComp(fdd, fddr))
480
+ {
481
+ m_imple->compFields.push_back(&fddr);
482
+ short index = (short)m_imple->compFields.size() - 1;
483
+ itm.indexTmp = index;
484
+ if (fdd.isNullable())
485
+ {
486
+ fielddef& fd = const_cast<fielddef&>(m_imple->compFields[index]);
487
+ fd.setNullable(true, fdd.isDefaultNull());
488
+ }
489
+ itm.cmpLen = fddr.len;
490
+ itm.compFunc = getCompFunc2(fddr, itm.compType);
491
+
492
+ }else
493
+ { // same type
494
+ const fielddef& fdest = (fdd.len < fddr.len) ? fdd : fddr;
495
+ itm.cmpLen = fdest.len;
496
+ itm.compFunc = getCompFunc2(fdest, itm.compType);
497
+ }
498
+ m_imple->compItems.push_back(itm);
499
+ }
500
+ if (m_imple->compFields.size())
501
+ createTempRecord();
502
+ }
503
+
504
+ void recordsetQuery::setJoinRow(const row_ptr row)
505
+ {
506
+ if (m_imple->row)
507
+ {
508
+ // Convert data type
509
+ for (size_t i = 0; i < m_imple->compItems.size(); ++i)
510
+ {
511
+ recordCompItem& itm = m_imple->compItems[i];
512
+ if (itm.indexTmp != -1)
513
+ {
514
+ field f = (*m_imple->row)[itm.indexTmp];
515
+ const field& fd = (*row)[itm.index];
516
+ if (fd.isNull())
517
+ f.setNull(true);
518
+ else
519
+ f = fd.c_str();
520
+ }
521
+ }
522
+ }
523
+ else
524
+ m_imple->joinRow = row;
525
+ }
367
526
 
368
- if (i + 3 < (int)tokns.size())
527
+ #define HINT_LEFT_NULL 1
528
+ /*
529
+ match for join
530
+ */
531
+ bool recordsetQuery::matchJoin(const row_ptr rrow) const
532
+ {
533
+ m_imple->matchHint = 0;
534
+ for (int i = 0; i < (int)m_imple->compItems.size(); ++i)
535
+ {
536
+ recordCompItem& itm = m_imple->compItems[i];
537
+ bool ret;
538
+ int nullJudge = 2;
539
+ const field& f = itm.indexTmp == -1 ? (*m_imple->joinRow)[itm.index] : (*m_imple->row)[itm.indexTmp];
540
+ const field& fr = (*rrow)[itm.cmpIndex];
541
+ if (itm.nullable)
542
+ nullJudge = f.nullCompMatch(fr, 0);
543
+ if (nullJudge < 2)
369
544
  {
370
- std::_tstring s = tokns[i + 3];
371
- if (s == _T("or"))
372
- itm.combine = eCor;
373
- else if (s == _T("and"))
374
- itm.combine = eCand;
545
+ ret = (nullJudge == 0);
546
+ m_imple->matchHint |= (nullJudge == -1) ? HINT_LEFT_NULL : 0;
375
547
  }
376
548
  else
377
- itm.combine = eCend;
378
- ++index;
549
+ {
550
+ nullJudge = itm.compFunc((const char*)f.ptr(), (const char*)fr.ptr(), itm.cmpLen);
551
+ ret = itm.isMatchFunc(nullJudge);
552
+ }
553
+ if (isEndComp(itm.combine, ret)) return ret;
379
554
  }
555
+ return true;
556
+ }
557
+
558
+ int recordsetQuery::matchStatus() const
559
+ {
560
+ if (m_imple->matchHint & HINT_LEFT_NULL && m_imple->compItems.size() == 1)
561
+ return JOIN_NO_MORERECORD;
562
+
563
+ return 0;
380
564
  }
381
565
 
382
566
  bool recordsetQuery::match(const row_ptr row) const
383
567
  {
384
568
  for (int i = 0; i < (int)m_imple->compItems.size(); ++i)
385
569
  {
386
- recordsetQueryImple::compItem& itm = m_imple->compItems[i];
570
+ recordCompItem& itm = m_imple->compItems[i];
387
571
  bool ret;
388
572
  int nullJudge = 2;
389
573
  const field& f = (*row)[itm.index];
390
- if (m_imple->mysqlnull && (itm.nullable || itm.nulllog))
574
+ if (itm.nullable || itm.nulllog)
391
575
  nullJudge = f.nullComp((eCompType)(itm.compType & 0xf));
392
576
  if (nullJudge < 2)
393
577
  ret = (nullJudge == 0) ? true : false;
394
578
  else
395
579
  {
396
580
  if (itm.compType & CMPLOGICAL_FIELD)
397
- ret = itm.isMatchFunc(itm.compFunc((const char*)f.ptr(), (const char*)(*row)[itm.cmpIndex].ptr(), f.len()));
581
+ ret = itm.isMatchFunc(itm.compFunc((const char*)f.ptr(), (const char*)(*row)[itm.cmpIndex].ptr(), itm.cmpLen));
398
582
  else
399
- ret = itm.isMatchFunc(itm.compFunc((const char*)f.ptr(), (const char*)((*m_imple->row)[i].ptr()), (*m_imple->row)[i].len()));
583
+ ret = itm.isMatchFunc(itm.compFunc((const char*)f.ptr(), (const char*)((*m_imple->row)[i].ptr()), itm.cmpLen));
400
584
  }
401
585
  if (isEndComp(itm.combine, ret)) return ret;
402
586
  }
@@ -31,6 +31,8 @@ namespace tdap
31
31
  namespace client
32
32
  {
33
33
 
34
+ #define JOIN_NO_MORERECORD 1
35
+
34
36
  class DLLLIB fieldNames
35
37
  {
36
38
 
@@ -108,8 +110,14 @@ class DLLLIB recordsetQuery : protected query
108
110
  friend class recordsetImple;
109
111
 
110
112
  struct recordsetQueryImple* m_imple;
113
+
114
+ void createTempRecord();
111
115
  void init(const fielddefs* fdinfo);
116
+ void init(const fielddefs* fdinfo, const fielddefs* rfdinfo);
112
117
  bool match(const row_ptr row) const;
118
+ bool matchJoin(const row_ptr rrow) const;
119
+ void setJoinRow(const row_ptr row);
120
+ int matchStatus() const;
113
121
 
114
122
  public:
115
123
  recordsetQuery();
@@ -36,7 +36,7 @@ namespace client
36
36
  {
37
37
 
38
38
  autoMemory::autoMemory() : refarymem(), ptr(0), endFieldIndex(NULL), size(0),
39
- owner(false)
39
+ usedIndex(0), owner(false)
40
40
  {
41
41
  }
42
42
 
@@ -45,7 +45,7 @@ void autoMemory::setParams(unsigned char* p, size_t s, short* endIndex, bool own
45
45
  if (owner)
46
46
  {
47
47
  delete[] ptr;
48
- delete endFieldIndex;
48
+ delete[] endFieldIndex;
49
49
  }
50
50
 
51
51
  ptr = p;
@@ -59,11 +59,24 @@ void autoMemory::setParams(unsigned char* p, size_t s, short* endIndex, bool own
59
59
  memcpy(ptr, p, size);
60
60
  else
61
61
  memset(ptr, 0, size + 1);
62
- endFieldIndex = new short;
62
+ endFieldIndex = new short[JOINLIMIT_PER_RECORD];
63
+ memset(endFieldIndex, 0, sizeof(short) * JOINLIMIT_PER_RECORD);
63
64
  if (endIndex != NULL)
64
- *endFieldIndex = *endIndex;
65
+ endFieldIndex[usedIndex] = *endIndex;
65
66
  }
66
-
67
+ }
68
+
69
+ short* autoMemory::appendEndFieldIndex(short value)
70
+ {
71
+ assert(owner);
72
+ endFieldIndex[++usedIndex] = value;
73
+ return endFieldIndex + (usedIndex);
74
+ }
75
+
76
+ void autoMemory::assignEndFieldIndex(short* p)
77
+ {
78
+ assert(owner);
79
+ memcpy(endFieldIndex, p, sizeof(short) * JOINLIMIT_PER_RECORD);
67
80
  }
68
81
 
69
82
  autoMemory::~autoMemory()
@@ -71,7 +84,7 @@ autoMemory::~autoMemory()
71
84
  if (owner)
72
85
  {
73
86
  delete[] ptr;
74
- delete endFieldIndex;
87
+ delete[] endFieldIndex;
75
88
  }
76
89
  }
77
90
 
@@ -44,9 +44,12 @@ public:
44
44
  void setParams(unsigned char* p, size_t s, short* endIndex, bool own);
45
45
 
46
46
  autoMemory& operator=(const bzs::db::protocol::tdap::client::autoMemory& p);
47
+ short* appendEndFieldIndex(short value);
48
+ void assignEndFieldIndex(short* p);
47
49
  unsigned char* ptr;
48
50
  short* endFieldIndex;
49
- unsigned int size ;
51
+ unsigned int size;
52
+ unsigned char usedIndex;
50
53
  bool owner ;
51
54
  static autoMemory* create(int n);
52
55
  static autoMemory* create();
@@ -134,6 +137,12 @@ class DLLLIB memoryRecord : public fieldsBase
134
137
  }
135
138
  }
136
139
 
140
+ inline void setInvalidMemblockLast()
141
+ {
142
+ int num = memBlockSize() -1;
143
+ m_InvalidFlags |= ((2L << num) | 1L);
144
+ }
145
+
137
146
  void releaseMemory();
138
147
 
139
148
  protected:
@@ -180,6 +180,18 @@ recordset& recordset::reverse()
180
180
  return *this;
181
181
  }
182
182
 
183
+ recordset& recordset::join(const recordset& rs, recordsetQuery& rq)
184
+ {
185
+ m_imple->nestedLoopJoin(*rs.m_imple, rq, true);
186
+ return *this;
187
+ }
188
+
189
+ recordset& recordset::outerJoin(const recordset& rs, recordsetQuery& rq)
190
+ {
191
+ m_imple->nestedLoopJoin(*rs.m_imple, rq, false);
192
+ return *this;
193
+ }
194
+
183
195
  void recordset::reserve(size_t size)
184
196
  {
185
197
  m_imple->reserve(size);
@@ -190,6 +202,11 @@ void recordset::appendField(const _TCHAR* name, int type, short len)
190
202
  m_imple->appendField(name, type, len);
191
203
  }
192
204
 
205
+ void recordset::appendField(const fielddef& fd)
206
+ {
207
+ m_imple->appendField(fd);
208
+ }
209
+
193
210
  recordset& recordset::operator+=(const recordset& r)
194
211
  {
195
212
  if (r.size() == 0)
@@ -71,8 +71,11 @@ public:
71
71
  const _TCHAR* name7 = NULL, const _TCHAR* name8 = NULL);
72
72
  recordset& orderBy(const sortFields& orders);
73
73
  recordset& reverse();
74
+ recordset& join(const recordset& rs, recordsetQuery& rq);
75
+ recordset& outerJoin(const recordset& rs, recordsetQuery& rq);
74
76
  void reserve(size_t size);
75
77
  void appendField(const _TCHAR* name, int type, short len);
78
+ void appendField(const fielddef& fd);
76
79
  recordset& operator+=(const recordset& r);
77
80
  void release();
78
81
  static recordset* create();