Merge ~bfrk/epics-base:remove-dbfl_type_rec into ~epics-core/epics-base/+git/epics-base:7.0

Proposed by Ben Franksen
Status: Superseded
Proposed branch: ~bfrk/epics-base:remove-dbfl_type_rec
Merge into: ~epics-core/epics-base/+git/epics-base:7.0
Prerequisite: ~dirk.zimoch/epics-base:dbChannelForDBLinks
Diff against target: 910 lines (+250/-281)
12 files modified
modules/database/src/ioc/db/dbAccess.c (+22/-17)
modules/database/src/ioc/db/dbChannel.c (+17/-55)
modules/database/src/ioc/db/dbChannel.h (+4/-3)
modules/database/src/ioc/db/dbEvent.c (+36/-33)
modules/database/src/ioc/db/dbExtractArray.c (+19/-49)
modules/database/src/ioc/db/dbExtractArray.h (+30/-5)
modules/database/src/ioc/db/db_field_log.h (+32/-27)
modules/database/src/std/filters/arr.c (+51/-73)
modules/database/src/std/filters/ts.c (+32/-9)
modules/database/test/ioc/db/dbChArrTest.cpp (+1/-1)
modules/database/test/std/filters/arrTest.cpp (+3/-3)
modules/database/test/std/filters/dbndTest.c (+3/-6)
Reviewer Review Type Date Requested Status
EPICS Core Developers Pending
Review via email: mp+381464@code.launchpad.net

This proposal supersedes a proposal from 2020-03-26.

This proposal has been superseded by a proposal from 2021-01-12.

Description of the change

This is just refactors, no new features yet.

To post a comment you must log in.
Revision history for this message
Ben Franksen (bfrk) wrote : Posted in a previous version of this proposal

I see now that the diff is dominated by my first commit that reformats dbAccess.c. The actual refactor makes only very few changes there. If this hurts I can rebase this patch out of the current branch. How do you people treat code reformatting patches? I did this one with my standard C indent config that very closely matches the standard EPICS base style.

Revision history for this message
Ben Franksen (bfrk) wrote : Posted in a previous version of this proposal

I rebased and resubmitted to get rid of the reformat commit. Will create a different merge request for that one.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote : Posted in a previous version of this proposal

Apropos reformat commit: Should we do a general reformat of EPICS base? For example get rid of all the tabs.

Revision history for this message
Ben Franksen (bfrk) wrote : Posted in a previous version of this proposal

> Apropos reformat commit: Should we do a general reformat of EPICS base? For
> example get rid of all the tabs.

I suggest to move this discussion to https://code.launchpad.net/~bfrk/epics-base/+git/epics-base/+merge/381153 or core-talk.

Revision history for this message
Ben Franksen (bfrk) wrote : Posted in a previous version of this proposal

I have renamed my branch back to write-filters and removed the latest commit.

Revision history for this message
Ben Franksen (bfrk) wrote :

Okay, fresh and clean. Fixed the problem Ralph pointed out (in https://code.launchpad.net/~bfrk/epics-base/+git/epics-base/+merge/381308), rebased everything and resubmitted. Also renamed the branch because this has nothing (yet) to do with write/put filters.

Revision history for this message
Andrew Johnson (anj) wrote :

I haven't tried building or using this, but overall it looks reasonable. Unfortunately I don't think we can afford to change the prototype of dbGetField() like this because it is a published API that other modules call, but AFAIK they only ever pass NULL for the final argument – see my diff comment below for suggestions.

Revision history for this message
Ben Franksen (bfrk) wrote :
Download full text (3.4 KiB)

Am 31.03.20 um 22:17 schrieb Andrew Johnson:
> I haven't tried building or using this, but overall it looks reasonable. Unfortunately I don't think we can afford to change the prototype of dbGetField() like this because it is a published API that other modules call, but AFAIK they only ever pass NULL for the final argument – see my diff comment below for suggestions.

I offered this particular patch (commit 7287a1f) as kind of "for
discussion", as I wasn't sure how much external code still uses this
API. I only saw that the record types and device supports in base no
longer call dbGetField directly. BTW, the void *pflin member must have
been added at some point in time (it wasn't there in 3.14) and that must
have taken its toll on external modules, too. But that was a "major"
version jump so perhaps the rules allowed that.

Anyway, I can easily remove this particular patch. It is pretty much
independent from anything else I did or plan to do.

On another note, my current (unfinished) attempts at adding put filters
also include the addition of a new parameter to dbPut and dbPutField.
This is a similarly breaking change, and I guess I should do something
about that before I propose that code for merging, right?

>> diff --git a/modules/database/src/ioc/db/db_field_log.h b/modules/database/src/ioc/db/db_field_log.h
>> index 1534517..a801142 100644
>> --- a/modules/database/src/ioc/db/db_field_log.h
>> +++ b/modules/database/src/ioc/db/db_field_log.h
>> @@ -81,6 +92,8 @@ struct dbfl_val {
>> * db_delete_field_log(). Any code which changes a dbfl_type_ref
>> * field log to another type, or to reference different data,
>> * must explicitly call the dtor function.
>> + * If the dtor is NULL, then this means the array data is still owned
>> + * by a record.
>
> Would it make sense to provide a macro that evaluates that condition? The code I saw earlier that I assume was checking this looked somewhat scary for after a clean-up.

I guess you mean e.g. in dbAccess.c:

+ if ((!pfl || (pfl->type==dbfl_type_ref && !pfl->u.r.dtor)) &&
+ paddr->pfldDes->special == SPC_DBADDR &&
+ (prset = dbGetRset(paddr)) &&
+ prset->get_array_info) {
+ status = prset->get_array_info(paddr, &no_elements, &offset);
+ } else {
         offset = 0;
     }

Yes, it probably isn't too obvious to the reader what goes on here. In a
later patch, I have used a local boolean variable a la

+ int pfl_has_copy = pfl && (pfl->type != dbfl_type_ref ||
pfl->u.r.dtor);

but I guess offering (and using) a macro here is the right thing. Will fix.

>> +<<<<<<< modules/database/src/std/filters/arr.c
>
> I think the conflict markers shown here are really virtual, I believe they will disappear if/when we merge Dirk's branch that is marked as a prerequisite of this one.

The conflict is with a change you made on 7.0 since Dirk has created his
feature branch:

commit 25bb966cbc4be1f2a47116a8b9ec940bc2270b12
Author: Andrew Johnson <email address hidden>
Date: Sat Mar 14 16:19:26 2020 -0500

    Use the dbChannel*() accessor macros in the array filter code
    instead of exposing the dbChannel innards unnecessarily.

I wasn't sure how to set up my branch so...

Read more...

85822f3... by Ben Franksen

add macro dbfl_has_copy to db_field_log.h and use it in dbAccess.c

It encapsulates the slightly tricky logic to decide whether a pointer
to a db_field_log has ownership of the data or not.

Revision history for this message
Ben Franksen (bfrk) wrote :

I removed the patch that changes the dbGetField API and added a new commit to address the point about "If the dtor is NULL, then this means the array data is still owned by a record".

The conflict is still there, pending a recommendation how to set up my branch properly.

Revision history for this message
Ben Franksen (bfrk) wrote :

Fixed a logical mistake in my last commit (rebased).

Revision history for this message
Ben Franksen (bfrk) wrote :

Since no-one came forward with a recommendation, I have now locally rebased all my changes such that they cleanly apply on branch 7.0. I did this by creating a new branch that forks off at the current HEAD of 7.0, then merging Dirk's branch dbChannelForDBLinks into that, and then replaying my changes, resolving the single conflict in the array filter implementation.

I have pushed this 'work' branch to my launchpad fork of epics base. All tests pass on all my intermediate versions.

Before I force-push the other branch refs (which are now on the work branch) to my fork (thereby updating all my merge requests), can somebody please tell me if this is what I should do?

Revision history for this message
Andrew Johnson (anj) wrote :

Core group 11/4: Will need some reworking after we have merged Dirk's other branch.

This will affect filters, breaking any external filter code (none known though).
The result looks cleaner so we like the idea in principle, but we can't merge it yet.

Revision history for this message
Ben Franksen (bfrk) wrote :

> The result looks cleaner so we like the idea in principle, but we can't merge
> it yet.

I can try to get this in shape (i.e. rebase it to the current HEAD). Are the prerequisites merged now? Should I move over to github (because of the automatic CI trigger there)?

Revision history for this message
Andrew Johnson (anj) wrote :

Hi Ben, yes I think everything else related has been merged now. Using GitHub instead of Launchpad does help with doing builds and running tests on our major architectures. Thanks!

Revision history for this message
Ben Franksen (bfrk) wrote :

Unmerged commits

85822f3... by Ben Franksen

add macro dbfl_has_copy to db_field_log.h and use it in dbAccess.c

It encapsulates the slightly tricky logic to decide whether a pointer
to a db_field_log has ownership of the data or not.

27fe3e4... by Ben Franksen

refactor db_field_log and filters to get rid of dbfl_type_rec

This refactor simplifies and streamlines the code associated with server
side filters. Apart from immediate benefits (clearer code, less duplication)
it is also hoped that this will make it easier to add write filters.

The data pointer dbfl_ref.field can now either point to a copy owned by a
filter, or it can point to the original data owned by a record. In the
latter case, the dbfl_ref.dtor is NULL.

The dbExtractArray* functions are unified to the single function
dbExtractArray and stripped of conversion functionality. This is redundant
because we always call dbGet after applying filters, which takes care of
conversion. Accordingly, dbChannelMakeArrayCopy is now obsolete and its
single use (in the ts filter) replaced with dbExtractArray. Instead, we add
the helper function dbChannelGetArrayInfo to wrap the common boilerplate
around calls to the get_array_info method, used in both arr.c and ts.c.

4ab9808... by Ben Franksen

make it clearer what the result of wrapArrayIndices will be

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/modules/database/src/ioc/db/dbAccess.c b/modules/database/src/ioc/db/dbAccess.c
2index d7e5d08..3f7554a 100644
3--- a/modules/database/src/ioc/db/dbAccess.c
4+++ b/modules/database/src/ioc/db/dbAccess.c
5@@ -339,7 +339,7 @@ static void getOptions(DBADDR *paddr, char **poriginal, long *options,
6 dbCommon *pcommon;
7 char *pbuffer = *poriginal;
8
9- if (!pfl || pfl->type == dbfl_type_rec)
10+ if (!pfl)
11 field_type = paddr->field_type;
12 else
13 field_type = pfl->field_type;
14@@ -349,7 +349,7 @@ static void getOptions(DBADDR *paddr, char **poriginal, long *options,
15 if( (*options) & DBR_STATUS ) {
16 unsigned short *pushort = (unsigned short *)pbuffer;
17
18- if (!pfl || pfl->type == dbfl_type_rec) {
19+ if (!pfl) {
20 *pushort++ = pcommon->stat;
21 *pushort++ = pcommon->sevr;
22 } else {
23@@ -383,7 +383,7 @@ static void getOptions(DBADDR *paddr, char **poriginal, long *options,
24 if( (*options) & DBR_TIME ) {
25 epicsUInt32 *ptime = (epicsUInt32 *)pbuffer;
26
27- if (!pfl || pfl->type == dbfl_type_rec) {
28+ if (!pfl) {
29 *ptime++ = pcommon->time.secPastEpoch;
30 *ptime++ = pcommon->time.nsec;
31 } else {
32@@ -904,22 +904,23 @@ long dbGet(DBADDR *paddr, short dbrType,
33 if (nRequest && *nRequest == 0)
34 return 0;
35
36- if (!pfl || pfl->type == dbfl_type_rec) {
37+ if (!pfl) {
38 field_type = paddr->field_type;
39 no_elements = capacity = paddr->no_elements;
40-
41- /* Update field info from record
42- * may modify paddr->pfield
43- */
44- if (paddr->pfldDes->special == SPC_DBADDR &&
45- (prset = dbGetRset(paddr)) &&
46- prset->get_array_info) {
47- status = prset->get_array_info(paddr, &no_elements, &offset);
48- } else
49- offset = 0;
50 } else {
51 field_type = pfl->field_type;
52 no_elements = capacity = pfl->no_elements;
53+ }
54+
55+ /* Update field info from record (if neccessary);
56+ * may modify paddr->pfield.
57+ */
58+ if (!dbfl_has_copy(pfl) &&
59+ paddr->pfldDes->special == SPC_DBADDR &&
60+ (prset = dbGetRset(paddr)) &&
61+ prset->get_array_info) {
62+ status = prset->get_array_info(paddr, &no_elements, &offset);
63+ } else {
64 offset = 0;
65 }
66
67@@ -951,7 +952,7 @@ long dbGet(DBADDR *paddr, short dbrType,
68 goto done;
69 }
70
71- if (!pfl || pfl->type == dbfl_type_rec) {
72+ if (!pfl) {
73 status = dbFastGetConvertRoutine[field_type][dbrType]
74 (paddr->pfield, pbuf, paddr);
75 } else {
76@@ -964,6 +965,7 @@ long dbGet(DBADDR *paddr, short dbrType,
77
78 localAddr.field_type = pfl->field_type;
79 localAddr.field_size = pfl->field_size;
80+ /* not used by dbFastConvert: */
81 localAddr.no_elements = pfl->no_elements;
82 if (pfl->type == dbfl_type_val)
83 localAddr.pfield = (char *) &pfl->u.v.field;
84@@ -979,6 +981,8 @@ long dbGet(DBADDR *paddr, short dbrType,
85 if (nRequest) {
86 if (no_elements < *nRequest)
87 *nRequest = no_elements;
88+ if (capacity < *nRequest)
89+ *nRequest = capacity;
90 n = *nRequest;
91 } else {
92 n = 1;
93@@ -995,8 +999,8 @@ long dbGet(DBADDR *paddr, short dbrType,
94 }
95 /* convert data into the caller's buffer */
96 if (n <= 0) {
97- ;/*do nothing*/
98- } else if (!pfl || pfl->type == dbfl_type_rec) {
99+ ; /*do nothing */
100+ } else if (!pfl) {
101 status = convert(paddr, pbuf, n, capacity, offset);
102 } else {
103 DBADDR localAddr = *paddr; /* Structure copy */
104@@ -1008,6 +1012,7 @@ long dbGet(DBADDR *paddr, short dbrType,
105
106 localAddr.field_type = pfl->field_type;
107 localAddr.field_size = pfl->field_size;
108+ /* not used by dbConvert, it uses the passed capacity instead: */
109 localAddr.no_elements = pfl->no_elements;
110 if (pfl->type == dbfl_type_val)
111 localAddr.pfield = (char *) &pfl->u.v.field;
112diff --git a/modules/database/src/ioc/db/dbChannel.c b/modules/database/src/ioc/db/dbChannel.c
113index b6f53f7..c71d842 100644
114--- a/modules/database/src/ioc/db/dbChannel.c
115+++ b/modules/database/src/ioc/db/dbChannel.c
116@@ -52,14 +52,12 @@ typedef struct parseContext {
117
118 static void *dbChannelFreeList;
119 static void *chFilterFreeList;
120-static void *dbchStringFreeList;
121
122 void dbChannelExit(void)
123 {
124 freeListCleanup(dbChannelFreeList);
125 freeListCleanup(chFilterFreeList);
126- freeListCleanup(dbchStringFreeList);
127- dbChannelFreeList = chFilterFreeList = dbchStringFreeList = NULL;
128+ dbChannelFreeList = chFilterFreeList = NULL;
129 }
130
131 void dbChannelInit (void)
132@@ -69,7 +67,6 @@ void dbChannelInit (void)
133
134 freeListInitPvt(&dbChannelFreeList, sizeof(dbChannel), 128);
135 freeListInitPvt(&chFilterFreeList, sizeof(chFilter), 64);
136- freeListInitPvt(&dbchStringFreeList, sizeof(epicsOldString), 128);
137 db_init_event_freelists();
138 }
139
140@@ -449,28 +446,6 @@ static long parseArrayRange(dbChannel* chan, const char *pname, const char **ppn
141 return status;
142 }
143
144-/* Stolen from dbAccess.c: */
145-static short mapDBFToDBR[DBF_NTYPES] =
146- {
147- /* DBF_STRING => */DBR_STRING,
148- /* DBF_CHAR => */DBR_CHAR,
149- /* DBF_UCHAR => */DBR_UCHAR,
150- /* DBF_SHORT => */DBR_SHORT,
151- /* DBF_USHORT => */DBR_USHORT,
152- /* DBF_LONG => */DBR_LONG,
153- /* DBF_ULONG => */DBR_ULONG,
154- /* DBF_INT64 => */DBR_INT64,
155- /* DBF_UINT64 => */DBR_UINT64,
156- /* DBF_FLOAT => */DBR_FLOAT,
157- /* DBF_DOUBLE => */DBR_DOUBLE,
158- /* DBF_ENUM, => */DBR_ENUM,
159- /* DBF_MENU, => */DBR_ENUM,
160- /* DBF_DEVICE => */DBR_ENUM,
161- /* DBF_INLINK => */DBR_STRING,
162- /* DBF_OUTLINK => */DBR_STRING,
163- /* DBF_FWDLINK => */DBR_STRING,
164- /* DBF_NOACCESS => */DBR_NOACCESS };
165-
166 dbChannel * dbChannelCreate(const char *name)
167 {
168 const char *pname = name;
169@@ -743,37 +718,24 @@ void dbChannelDelete(dbChannel *chan)
170 freeListFree(dbChannelFreeList, chan);
171 }
172
173-static void freeArray(db_field_log *pfl) {
174- if (pfl->field_type == DBF_STRING && pfl->no_elements == 1) {
175- freeListFree(dbchStringFreeList, pfl->u.r.field);
176- } else {
177- free(pfl->u.r.field);
178- }
179-}
180-
181-void dbChannelMakeArrayCopy(void *pvt, db_field_log *pfl, dbChannel *chan)
182+/*
183+ * Helper function to adjust no_elements, offset, and pfield
184+ * when copying an array from a record.
185+ */
186+void dbChannelGetArrayInfo(dbChannel *chan,
187+ void **pfield, long *no_elements, long *offset)
188 {
189- void *p;
190- struct dbCommon *prec = dbChannelRecord(chan);
191-
192- if (pfl->type != dbfl_type_rec) return;
193-
194- pfl->type = dbfl_type_ref;
195- pfl->stat = prec->stat;
196- pfl->sevr = prec->sevr;
197- pfl->time = prec->time;
198- pfl->field_type = chan->addr.field_type;
199- pfl->no_elements = chan->addr.no_elements;
200- pfl->field_size = chan->addr.field_size;
201- pfl->u.r.dtor = freeArray;
202- pfl->u.r.pvt = pvt;
203- if (pfl->field_type == DBF_STRING && pfl->no_elements == 1) {
204- p = freeListCalloc(dbchStringFreeList);
205- } else {
206- p = calloc(pfl->no_elements, pfl->field_size);
207+ rset *prset;
208+ if (dbChannelSpecial(chan) == SPC_DBADDR &&
209+ (prset = dbGetRset(&chan->addr)) &&
210+ prset->get_array_info)
211+ {
212+ void *pfieldsave = dbChannelField(chan);
213+ /* it is expected that this call always succeeds */
214+ prset->get_array_info(&chan->addr, no_elements, offset);
215+ *pfield = dbChannelField(chan);
216+ dbChannelField(chan) = pfieldsave;
217 }
218- if (p) dbGet(&chan->addr, mapDBFToDBR[pfl->field_type], p, NULL, &pfl->no_elements, NULL);
219- pfl->u.r.field = p;
220 }
221
222 /* FIXME: Do these belong in a different file? */
223diff --git a/modules/database/src/ioc/db/dbChannel.h b/modules/database/src/ioc/db/dbChannel.h
224index 1ca02bb..ec86e9e 100644
225--- a/modules/database/src/ioc/db/dbChannel.h
226+++ b/modules/database/src/ioc/db/dbChannel.h
227@@ -65,8 +65,8 @@ typedef struct dbChannel {
228 /* Prototype for the channel event function that is called in filter stacks
229 *
230 * When invoked the scan lock for the record associated with 'chan' _may_ be locked.
231- * If pLog->type==dbfl_type_rec then dbScanLock() must be called before copying
232- * data out of the associated record.
233+ * Unless dbfl_has_copy(pLog), it must call dbScanLock before accessing the data,
234+ * as this indicates the data is still owned by the record.
235 *
236 * This function has ownership of the field log pLog, if it wishes to discard
237 * this update it should free the field log with db_delete_field_log() and
238@@ -225,7 +225,8 @@ DBCORE_API void dbRegisterFilter(const char *key, const chFilterIf *fif, void *p
239 DBCORE_API db_field_log* dbChannelRunPreChain(dbChannel *chan, db_field_log *pLogIn);
240 DBCORE_API db_field_log* dbChannelRunPostChain(dbChannel *chan, db_field_log *pLogIn);
241 DBCORE_API const chFilterPlugin * dbFindFilter(const char *key, size_t len);
242-DBCORE_API void dbChannelMakeArrayCopy(void *pvt, db_field_log *pfl, dbChannel *chan);
243+DBCORE_API void dbChannelGetArrayInfo(dbChannel *chan,
244+ void **pfield, long *no_elements, long *offset);
245
246 #ifdef __cplusplus
247 }
248diff --git a/modules/database/src/ioc/db/dbEvent.c b/modules/database/src/ioc/db/dbEvent.c
249index 437cb6d..1db01f2 100644
250--- a/modules/database/src/ioc/db/dbEvent.c
251+++ b/modules/database/src/ioc/db/dbEvent.c
252@@ -668,27 +668,21 @@ int db_post_extra_labor (dbEventCtx ctx)
253 return DB_EVENT_OK;
254 }
255
256-/*
257- * DB_CREATE_EVENT_LOG()
258- *
259- * NOTE: This assumes that the db scan lock is already applied
260- * (as it copies data from the record)
261- */
262-db_field_log* db_create_event_log (struct evSubscrip *pevent)
263+static db_field_log* db_create_field_log (struct dbChannel *chan, int use_val)
264 {
265 db_field_log *pLog = (db_field_log *) freeListCalloc(dbevFieldLogFreeList);
266
267 if (pLog) {
268- struct dbChannel *chan = pevent->chan;
269 struct dbCommon *prec = dbChannelRecord(chan);
270- pLog->ctx = dbfl_context_event;
271- if (pevent->useValque) {
272+ pLog->stat = prec->stat;
273+ pLog->sevr = prec->sevr;
274+ pLog->time = prec->time;
275+ pLog->field_type = dbChannelFieldType(chan);
276+ pLog->field_size = dbChannelFieldSize(chan);
277+ pLog->no_elements = dbChannelElements(chan);
278+
279+ if (use_val) {
280 pLog->type = dbfl_type_val;
281- pLog->stat = prec->stat;
282- pLog->sevr = prec->sevr;
283- pLog->time = prec->time;
284- pLog->field_type = dbChannelFieldType(chan);
285- pLog->no_elements = dbChannelElements(chan);
286 /*
287 * use memcpy to avoid a bus error on
288 * union copy of char in the db at an odd
289@@ -698,23 +692,46 @@ db_field_log* db_create_event_log (struct evSubscrip *pevent)
290 dbChannelField(chan),
291 dbChannelFieldSize(chan));
292 } else {
293- pLog->type = dbfl_type_rec;
294+ pLog->type = dbfl_type_ref;
295+
296+ /* don't make a copy yet, just reference the field value */
297+ pLog->u.r.field = dbChannelField(chan);
298+ /* indicate field value still owned by record */
299+ pLog->u.r.dtor = NULL;
300+ /* no private data yet, may be set by a filter */
301+ pLog->u.r.pvt = NULL;
302 }
303 }
304 return pLog;
305 }
306
307 /*
308+ * DB_CREATE_EVENT_LOG()
309+ *
310+ * NOTE: This assumes that the db scan lock is already applied
311+ * (as it calls rset->get_array_info)
312+ */
313+db_field_log* db_create_event_log (struct evSubscrip *pevent)
314+{
315+ db_field_log *pLog = db_create_field_log(pevent->chan, pevent->useValque);
316+ if (pLog) {
317+ pLog->ctx = dbfl_context_event;
318+ }
319+ return pLog;
320+}
321+
322+/*
323 * DB_CREATE_READ_LOG()
324 *
325 */
326 db_field_log* db_create_read_log (struct dbChannel *chan)
327 {
328- db_field_log *pLog = (db_field_log *) freeListCalloc(dbevFieldLogFreeList);
329-
330+ db_field_log *pLog = db_create_field_log(chan,
331+ dbChannelElements(chan) == 1 &&
332+ dbChannelSpecial(chan) != SPC_DBADDR &&
333+ dbChannelFieldSize(chan) <= sizeof(union native_value));
334 if (pLog) {
335 pLog->ctx = dbfl_context_read;
336- pLog->type = dbfl_type_rec;
337 }
338 return pLog;
339 }
340@@ -738,20 +755,6 @@ static void db_queue_event_log (evSubscrip *pevent, db_field_log *pLog)
341 LOCKEVQUE (ev_que);
342
343 /*
344- * if we have an event on the queue and both the last
345- * event on the queue and the current event are emtpy
346- * (i.e. of type dbfl_type_rec), simply ignore duplicate
347- * events (saving empty events serves no purpose)
348- */
349- if (pevent->npend > 0u &&
350- (*pevent->pLastLog)->type == dbfl_type_rec &&
351- pLog->type == dbfl_type_rec) {
352- db_delete_field_log(pLog);
353- UNLOCKEVQUE (ev_que);
354- return;
355- }
356-
357- /*
358 * add to task local event que
359 */
360
361diff --git a/modules/database/src/ioc/db/dbExtractArray.c b/modules/database/src/ioc/db/dbExtractArray.c
362index a7dcf4d..197e66c 100644
363--- a/modules/database/src/ioc/db/dbExtractArray.c
364+++ b/modules/database/src/ioc/db/dbExtractArray.c
365@@ -14,11 +14,12 @@
366 /*
367 * Author: Ralph Lange <Ralph.Lange@bessy.de>
368 *
369- * based on dbConvert.c
370+ * based on dbConvert.c, see copyNoConvert
371 * written by: Bob Dalesio, Marty Kraimer
372 */
373
374 #include <string.h>
375+#include <assert.h>
376
377 #include "epicsTypes.h"
378
379@@ -26,61 +27,30 @@
380 #include "dbAddr.h"
381 #include "dbExtractArray.h"
382
383-void dbExtractArrayFromRec(const dbAddr *paddr, void *pto,
384- long nRequest, long no_elements, long offset, long increment)
385+void dbExtractArray(const void *pfrom, void *pto, short field_size,
386+ long nRequest, long no_elements, long offset, long increment)
387 {
388 char *pdst = (char *) pto;
389- char *psrc = (char *) paddr->pfield;
390- long nUpperPart;
391- int i;
392- short srcSize = paddr->field_size;
393- short dstSize = srcSize;
394- char isString = (paddr->field_type == DBF_STRING);
395+ const char *psrc = (char *) pfrom;
396
397- if (nRequest > no_elements) nRequest = no_elements;
398- if (isString && srcSize > MAX_STRING_SIZE) dstSize = MAX_STRING_SIZE;
399+ /* assert preconditions */
400+ assert(nRequest >= 0);
401+ assert(no_elements >= 0);
402+ assert(increment > 0);
403+ assert(0 <= offset);
404+ assert(offset < no_elements);
405
406- if (increment == 1 && dstSize == srcSize) {
407- nUpperPart = nRequest < no_elements - offset ? nRequest : no_elements - offset;
408- memcpy(pdst, &psrc[offset * srcSize], dstSize * nUpperPart);
409+ if (increment == 1) {
410+ long nUpperPart =
411+ nRequest < no_elements - offset ? nRequest : no_elements - offset;
412+ memcpy(pdst, psrc + (offset * field_size), field_size * nUpperPart);
413 if (nRequest > nUpperPart)
414- memcpy(&pdst[dstSize * nUpperPart], psrc, dstSize * (nRequest - nUpperPart));
415- if (isString)
416- for (i = 1; i <= nRequest; i++)
417- pdst[dstSize*i-1] = '\0';
418+ memcpy(pdst + (field_size * nUpperPart), psrc,
419+ field_size * (nRequest - nUpperPart));
420 } else {
421- for (; nRequest > 0; nRequest--, pdst += dstSize, offset += increment) {
422+ for (; nRequest > 0; nRequest--, pdst += field_size, offset += increment) {
423 offset %= no_elements;
424- memcpy(pdst, &psrc[offset*srcSize], dstSize);
425- if (isString) pdst[dstSize-1] = '\0';
426- }
427- }
428-}
429-
430-void dbExtractArrayFromBuf(const void *pfrom, void *pto,
431- short field_size, short field_type,
432- long nRequest, long no_elements, long offset, long increment)
433-{
434- char *pdst = (char *) pto;
435- char *psrc = (char *) pfrom;
436- int i;
437- short srcSize = field_size;
438- short dstSize = srcSize;
439- char isString = (field_type == DBF_STRING);
440-
441- if (nRequest > no_elements) nRequest = no_elements;
442- if (offset > no_elements - 1) offset = no_elements - 1;
443- if (isString && dstSize >= MAX_STRING_SIZE) dstSize = MAX_STRING_SIZE - 1;
444-
445- if (increment == 1) {
446- memcpy(pdst, &psrc[offset * srcSize], dstSize * nRequest);
447- if (isString)
448- for (i = 1; i <= nRequest; i++)
449- pdst[dstSize*i] = '\0';
450- } else {
451- for (; nRequest > 0; nRequest--, pdst += srcSize, offset += increment) {
452- memcpy(pdst, &psrc[offset*srcSize], dstSize);
453- if (isString) pdst[dstSize] = '\0';
454+ memcpy(pdst, psrc + (offset * field_size), field_size);
455 }
456 }
457 }
458diff --git a/modules/database/src/ioc/db/dbExtractArray.h b/modules/database/src/ioc/db/dbExtractArray.h
459index b44bd15..9755ac5 100644
460--- a/modules/database/src/ioc/db/dbExtractArray.h
461+++ b/modules/database/src/ioc/db/dbExtractArray.h
462@@ -22,11 +22,36 @@
463 extern "C" {
464 #endif
465
466-epicsShareFunc void dbExtractArrayFromRec(const DBADDR *paddr, void *pto,
467- long nRequest, long no_elements, long offset, long increment);
468-epicsShareFunc void dbExtractArrayFromBuf(const void *pfrom, void *pto,
469- short field_size, short field_type,
470- long nRequest, long no_elements, long offset, long increment);
471+/** @brief Make a copy of parts of an array.
472+ *
473+ * The source array may or may not be a record field.
474+ *
475+ * The increment parameter is used to support array filters; it
476+ * means: copy only every increment'th element, starting at offset.
477+ *
478+ * The offset and no_elements parameters are used to support the
479+ * circular buffer feature of record fields: elements before offset
480+ * are treated as if they came right after no_elements.
481+ *
482+ * This function does not do any conversion on the array elements.
483+ *
484+ * Preconditions:
485+ * nRequest >= 0, no_elements >= 0, increment > 0
486+ * 0 <= offset < no_elements
487+ * pto points to a buffer with at least field_size*nRequest bytes
488+ * pfrom points to a buffer with exactly field_size*no_elements bytes
489+ *
490+ * @param pfrom Pointer to source array.
491+ * @param pto Pointer to target array.
492+ * @param field_size Size of an array element.
493+ * @param nRequest Number of elements to copy.
494+ * @param no_elements Number of elements in source array.
495+ * @param offset Wrap-around point in source array.
496+ * @param increment Copy only every increment'th element.
497+ */
498+epicsShareFunc void dbExtractArray(const void *pfrom, void *pto,
499+ short field_size, long nRequest, long no_elements, long offset,
500+ long increment);
501
502 #ifdef __cplusplus
503 }
504diff --git a/modules/database/src/ioc/db/db_field_log.h b/modules/database/src/ioc/db/db_field_log.h
505index 5a40f5f..222bad0 100644
506--- a/modules/database/src/ioc/db/db_field_log.h
507+++ b/modules/database/src/ioc/db/db_field_log.h
508@@ -57,20 +57,31 @@ union native_value {
509 struct db_field_log;
510 typedef void (dbfl_freeFunc)(struct db_field_log *pfl);
511
512-/* Types of db_field_log: rec = use record, val = val inside, ref = reference inside */
513+/*
514+ * A db_field_log has one of two types:
515+ *
516+ * dbfl_type_ref - Reference to value
517+ * Used for variable size (array) data types. Meta-data
518+ * is stored in the field log, but value data is stored externally.
519+ * Only the dbfl_ref side of the data union is valid.
520+ *
521+ * dbfl_type_val - Internal value
522+ * Used to store small scalar data. Meta-data and value are
523+ * present in this structure and no external references are used.
524+ * Only the dbfl_val side of the data union is valid.
525+ */
526 typedef enum dbfl_type {
527- dbfl_type_rec = 0,
528 dbfl_type_val,
529 dbfl_type_ref
530 } dbfl_type;
531
532 /* Context of db_field_log: event = subscription update, read = read reply */
533 typedef enum dbfl_context {
534- dbfl_context_read = 0,
535+ dbfl_context_read,
536 dbfl_context_event
537 } dbfl_context;
538
539-#define dbflTypeStr(t) (t==dbfl_type_val?"val":t==dbfl_type_rec?"rec":"ref")
540+#define dbflTypeStr(t) (t==dbfl_type_val?"val":"ref")
541
542 struct dbfl_val {
543 union native_value field; /* Field value */
544@@ -82,6 +93,8 @@ struct dbfl_val {
545 * db_delete_field_log(). Any code which changes a dbfl_type_ref
546 * field log to another type, or to reference different data,
547 * must explicitly call the dtor function.
548+ * If the dtor is NULL and no_elements > 0, then this means the array
549+ * data is still owned by a record. See the macro dbfl_has_copy below.
550 */
551 struct dbfl_ref {
552 dbfl_freeFunc *dtor; /* Callback to free filter-allocated resources */
553@@ -89,8 +102,11 @@ struct dbfl_ref {
554 void *field; /* Field value */
555 };
556
557+/*
558+ * Note that field_size may be larger than MAX_STRING_SIZE.
559+ */
560 typedef struct db_field_log {
561- unsigned int type:2; /* type (union) selector */
562+ unsigned int type:1; /* type (union) selector */
563 /* ctx is used for all types */
564 unsigned int ctx:1; /* context (operation type) */
565 /* the following are used for value and reference types */
566@@ -98,8 +114,8 @@ typedef struct db_field_log {
567 unsigned short stat; /* Alarm Status */
568 unsigned short sevr; /* Alarm Severity */
569 short field_type; /* DBF type of data */
570- short field_size; /* Data size */
571- long no_elements; /* No of array elements */
572+ short field_size; /* Size of a single element */
573+ long no_elements; /* No of valid array elements */
574 union {
575 struct dbfl_val v;
576 struct dbfl_ref r;
577@@ -107,27 +123,16 @@ typedef struct db_field_log {
578 } db_field_log;
579
580 /*
581- * A db_field_log will in one of three types:
582- *
583- * dbfl_type_rec - Reference to record
584- * The field log stores no data itself. Data must instead be taken
585- * via the dbChannel* which must always be provided when along
586- * with the field log.
587- * For this type only the 'type' and 'ctx' members are used.
588- *
589- * dbfl_type_ref - Reference to outside value
590- * Used for variable size (array) data types. Meta-data
591- * is stored in the field log, but value data is stored externally
592- * (see struct dbfl_ref).
593- * For this type all meta-data members are used. The dbfl_ref side of the
594- * data union is used.
595- *
596- * dbfl_type_val - Internal value
597- * Used to store small scalar data. Meta-data and value are
598- * present in this structure and no external references are used.
599- * For this type all meta-data members are used. The dbfl_val side of the
600- * data union is used.
601+ * Whether a db_field_log* owns the field data. If this is the case, then the
602+ * db_field_log is fully decoupled from the record: there is no need to lock
603+ * the record when accessing the data, nor to call any rset methods (like
604+ * get_array_info) because this has already been done when we made the copy. A
605+ * special case here is that of no (remaining) data (i.e. no_elements==0). In
606+ * this case, making a copy is redundant, so we have no dtor. But conceptually
607+ * the db_field_log still owns the (empty) data.
608 */
609+#define dbfl_has_copy(p)\
610+ ((p) && ((p)->type==dbfl_type_val || (p)->u.r.dtor || (p)->no_elements==0))
611
612 #ifdef __cplusplus
613 }
614diff --git a/modules/database/src/std/filters/arr.c b/modules/database/src/std/filters/arr.c
615index 8138480..5f25184 100644
616--- a/modules/database/src/std/filters/arr.c
617+++ b/modules/database/src/std/filters/arr.c
618@@ -13,16 +13,14 @@
619
620 #include <stdio.h>
621
622-#include <freeList.h>
623-#include <dbAccess.h>
624-#include <dbExtractArray.h>
625-#include <db_field_log.h>
626-#include <dbLock.h>
627-#include <recSup.h>
628-#include <epicsExit.h>
629-#include <special.h>
630-#include <chfPlugin.h>
631-#include <epicsExport.h>
632+#include "chfPlugin.h"
633+#include "dbAccessDefs.h"
634+#include "dbExtractArray.h"
635+#include "db_field_log.h"
636+#include "dbLock.h"
637+#include "epicsExit.h"
638+#include "freeList.h"
639+#include "epicsExport.h"
640
641 typedef struct myStruct {
642 epicsInt32 start;
643@@ -46,6 +44,8 @@ static void * allocPvt(void)
644 myStruct *my = (myStruct*) freeListCalloc(myStructFreeList);
645 if (!my) return NULL;
646
647+ /* defaults */
648+ my->start = 0;
649 my->incr = 1;
650 my->end = -1;
651 return (void *) my;
652@@ -77,8 +77,6 @@ static void freeArray(db_field_log *pfl)
653 static long wrapArrayIndices(long *start, const long increment, long *end,
654 const long no_elements)
655 {
656- long len = 0;
657-
658 if (*start < 0) *start = no_elements + *start;
659 if (*start < 0) *start = 0;
660 if (*start > no_elements) *start = no_elements;
661@@ -87,85 +85,65 @@ static long wrapArrayIndices(long *start, const long increment, long *end,
662 if (*end < 0) *end = 0;
663 if (*end >= no_elements) *end = no_elements - 1;
664
665- if (*end - *start >= 0) len = 1 + (*end - *start) / increment;
666- return len;
667+ if (*end - *start >= 0)
668+ return 1 + (*end - *start) / increment;
669+ else
670+ return 0;
671 }
672
673 static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl)
674 {
675 myStruct *my = (myStruct*) pvt;
676- struct dbCommon *prec;
677- rset *prset;
678+ int must_lock;
679 long start = my->start;
680 long end = my->end;
681- long nTarget = 0;
682+ long nTarget;
683+ void *pTarget;
684 long offset = 0;
685- long nSource = dbChannelElements(chan);
686- long capacity = nSource;
687- void *pdst;
688+ long nSource = pfl->no_elements;
689+ void *pSource = pfl->u.r.field;
690
691 switch (pfl->type) {
692 case dbfl_type_val:
693- /* Only filter arrays */
694- break;
695-
696- case dbfl_type_rec:
697- /* Extract from record */
698- if (dbChannelSpecial(chan) == SPC_DBADDR &&
699- nSource > 1 &&
700- (prset = dbGetRset(&chan->addr)) &&
701- prset->get_array_info)
702- {
703- void *pfieldsave = dbChannelField(chan);
704- prec = dbChannelRecord(chan);
705- dbScanLock(prec);
706- prset->get_array_info(&chan->addr, &nSource, &offset);
707- nTarget = wrapArrayIndices(&start, my->incr, &end, nSource);
708- pfl->type = dbfl_type_ref;
709- pfl->stat = prec->stat;
710- pfl->sevr = prec->sevr;
711- pfl->time = prec->time;
712- pfl->field_type = dbChannelFieldType(chan);
713- pfl->field_size = dbChannelFieldSize(chan);
714- pfl->no_elements = nTarget;
715- if (nTarget) {
716- pdst = freeListCalloc(my->arrayFreeList);
717- if (pdst) {
718- pfl->u.r.dtor = freeArray;
719- pfl->u.r.pvt = my->arrayFreeList;
720- offset = (offset + start) % dbChannelElements(chan);
721- dbExtractArrayFromRec(&chan->addr, pdst, nTarget, capacity,
722- offset, my->incr);
723- pfl->u.r.field = pdst;
724- }
725- }
726- dbScanUnlock(prec);
727- dbChannelField(chan) = pfieldsave;
728- }
729+ /* TODO Treat scalars as arrays with 1 element */
730 break;
731
732- /* Extract from buffer */
733 case dbfl_type_ref:
734- pdst = NULL;
735- nSource = pfl->no_elements;
736- nTarget = wrapArrayIndices(&start, my->incr, &end, nSource);
737- pfl->no_elements = nTarget;
738- if (nTarget) {
739- /* Copy the data out */
740- void *psrc = pfl->u.r.field;
741-
742- pdst = freeListCalloc(my->arrayFreeList);
743- if (!pdst) break;
744- offset = start;
745- dbExtractArrayFromBuf(psrc, pdst, pfl->field_size, pfl->field_type,
746- nTarget, nSource, offset, my->incr);
747+ must_lock = !pfl->u.r.dtor;
748+ if (must_lock) {
749+ dbScanLock(dbChannelRecord(chan));
750+ dbChannelGetArrayInfo(chan, &pSource, &nSource, &offset);
751 }
752- if (pfl->u.r.dtor) pfl->u.r.dtor(pfl);
753- if (nTarget) {
754+ nTarget = wrapArrayIndices(&start, my->incr, &end, nSource);
755+ if (nTarget > 0) {
756+ /* copy the data */
757+ pTarget = freeListCalloc(my->arrayFreeList);
758+ if (!pTarget) break;
759+ /* must do the wrap-around with the original no_elements */
760+ offset = (offset + start) % pfl->no_elements;
761+ dbExtractArray(pSource, pTarget, pfl->field_size,
762+ nTarget, pfl->no_elements, offset, my->incr);
763+ if (pfl->u.r.dtor) pfl->u.r.dtor(pfl);
764+ pfl->u.r.field = pTarget;
765 pfl->u.r.dtor = freeArray;
766 pfl->u.r.pvt = my->arrayFreeList;
767- pfl->u.r.field = pdst;
768 }
769+ /* Adjust no_elements to refer to the new pTarget.
770+ *
771+ * Setting pfl->no_elements outside of the "if" clause above is
772+ * done to make requests fail if nTarget is zero, that is, if all
773+ * elements selected by the filter are outside the array bounds.
774+ * TODO:
775+ * It would be possible to lift this restriction by interpreting
776+ * a request with *no* number of elements (NULL pointer) as scalar
777+ * (meaning: fail if we get less than one element); in contrast,
778+ * a request that explicitly specifies one element would be
779+ * interpreted as an array request, for which zero elements would
780+ * be a normal expected result.
781+ */
782+ pfl->no_elements = nTarget;
783+ if (must_lock)
784+ dbScanUnlock(dbChannelRecord(chan));
785 break;
786 }
787 return pfl;
788diff --git a/modules/database/src/std/filters/ts.c b/modules/database/src/std/filters/ts.c
789index 8a07c3f..0feadb0 100644
790--- a/modules/database/src/std/filters/ts.c
791+++ b/modules/database/src/std/filters/ts.c
792@@ -12,21 +12,44 @@
793 */
794
795 #include <stdio.h>
796+#include <stdlib.h>
797+#include <string.h>
798
799-#include <chfPlugin.h>
800-#include <dbLock.h>
801-#include <db_field_log.h>
802-#include <epicsExport.h>
803+#include "chfPlugin.h"
804+#include "db_field_log.h"
805+#include "dbExtractArray.h"
806+#include "dbLock.h"
807+#include "epicsExport.h"
808+
809+/*
810+ * The size of the data is different for each channel, and can even
811+ * change at runtime, so a freeList doesn't make much sense here.
812+ */
813+static void freeArray(db_field_log *pfl) {
814+ free(pfl->u.r.field);
815+}
816
817 static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl) {
818 epicsTimeStamp now;
819 epicsTimeGetCurrent(&now);
820
821- /* If string or array, must make a copy (to ensure coherence between time and data) */
822- if (pfl->type == dbfl_type_rec) {
823- dbScanLock(dbChannelRecord(chan));
824- dbChannelMakeArrayCopy(pvt, pfl, chan);
825- dbScanUnlock(dbChannelRecord(chan));
826+ /* If reference and not already copied,
827+ must make a copy (to ensure coherence between time and data) */
828+ if (pfl->type == dbfl_type_ref && !pfl->u.r.dtor) {
829+ void *pTarget = calloc(pfl->no_elements, pfl->field_size);
830+ void *pSource = pfl->u.r.field;
831+ if (pTarget) {
832+ long offset = 0;
833+ long nSource = pfl->no_elements;
834+ dbScanLock(dbChannelRecord(chan));
835+ dbChannelGetArrayInfo(chan, &pSource, &nSource, &offset);
836+ dbExtractArray(pSource, pTarget, pfl->field_size,
837+ nSource, pfl->no_elements, offset, 1);
838+ pfl->u.r.field = pTarget;
839+ pfl->u.r.dtor = freeArray;
840+ pfl->u.r.pvt = pvt;
841+ dbScanUnlock(dbChannelRecord(chan));
842+ }
843 }
844
845 pfl->time = now;
846diff --git a/modules/database/test/ioc/db/dbChArrTest.cpp b/modules/database/test/ioc/db/dbChArrTest.cpp
847index 90702b5..9965852 100644
848--- a/modules/database/test/ioc/db/dbChArrTest.cpp
849+++ b/modules/database/test/ioc/db/dbChArrTest.cpp
850@@ -131,7 +131,7 @@ static void check(short dbr_type) {
851 memset(buf, 0, sizeof(buf)); \
852 (void) dbPutField(&offaddr, DBR_LONG, &off, 1); \
853 pfl = db_create_read_log(pch); \
854- testOk(pfl && pfl->type == dbfl_type_rec, "Valid pfl, type = rec"); \
855+ testOk(pfl && pfl->type == dbfl_type_ref, "Valid pfl, type = ref"); \
856 testOk(!dbChannelGetField(pch, DBR_LONG, buf, NULL, &req, pfl), "Got Field value"); \
857 testOk(req == Size, "Got %ld elements (expected %d)", req, Size); \
858 if (!testOk(!memcmp(buf, Expected, sizeof(Expected)), "Data correct")) \
859diff --git a/modules/database/test/std/filters/arrTest.cpp b/modules/database/test/std/filters/arrTest.cpp
860index bd83bd8..dfbbf46 100644
861--- a/modules/database/test/std/filters/arrTest.cpp
862+++ b/modules/database/test/std/filters/arrTest.cpp
863@@ -73,9 +73,9 @@ static int fl_equals_array(short type, const db_field_log *pfl1, void *p2) {
864 }
865 break;
866 case DBR_STRING:
867- if (strtol(&((const char*)pfl1->u.r.field)[i*MAX_STRING_SIZE], NULL, 0) != ((epicsInt32*)p2)[i]) {
868+ if (strtol(&((const char*)pfl1->u.r.field)[i*pfl1->field_size], NULL, 0) != ((epicsInt32*)p2)[i]) {
869 testDiag("at index=%d: field log has '%s', should be '%d'",
870- i, &((const char*)pfl1->u.r.field)[i*MAX_STRING_SIZE], ((epicsInt32*)p2)[i]);
871+ i, &((const char*)pfl1->u.r.field)[i*pfl1->field_size], ((epicsInt32*)p2)[i]);
872 return 0;
873 }
874 break;
875@@ -120,7 +120,7 @@ static void testHead (const char *title, const char *typ = "") {
876 off = Offset; \
877 (void) dbPutField(&offaddr, DBR_LONG, &off, 1); \
878 pfl = db_create_read_log(pch); \
879- testOk(pfl->type == dbfl_type_rec, "original field log has type rec"); \
880+ testOk(pfl->type == dbfl_type_ref, "original field log has type ref"); \
881 pfl2 = dbChannelRunPostChain(pch, pfl); \
882 testOk(pfl2 == pfl, "call does not drop or replace field_log"); \
883 testOk(pfl->type == dbfl_type_ref, "filtered field log has type ref"); \
884diff --git a/modules/database/test/std/filters/dbndTest.c b/modules/database/test/std/filters/dbndTest.c
885index e7897e2..0206865 100644
886--- a/modules/database/test/std/filters/dbndTest.c
887+++ b/modules/database/test/std/filters/dbndTest.c
888@@ -130,7 +130,7 @@ MAIN(dbndTest)
889 dbEventCtx evtctx;
890 int logsFree, logsFinal;
891
892- testPlan(77);
893+ testPlan(72);
894
895 testdbPrepare();
896
897@@ -171,12 +171,9 @@ MAIN(dbndTest)
898 "dbnd has one filter with argument in pre chain");
899 testOk((ellCount(&pch->post_chain) == 0), "dbnd has no filter in post chain");
900
901- /* Field logs of type ref and rec: pass any update */
902-
903- testHead("Field logs of type ref and rec");
904- fl1.type = dbfl_type_rec;
905- mustPassTwice(pch, &fl1, "abs field_log=rec", 0., 0);
906+ /* Field logs of type ref: pass any update */
907
908+ testHead("Field logs of type ref");
909 fl1.type = dbfl_type_ref;
910 mustPassTwice(pch, &fl1, "abs field_log=ref", 0., 0);
911

Subscribers

People subscribed via source and target branches