Bug #7473
rewrite toString() implementation for Dataset, StaticTempTable and TempTableBuilder to be pure-sql and don't rely on WRITE-JSON
100%
History
#2 Updated by Constantin Asofiei about 1 year ago
Current implementation relies on WRITE-JSON, which has side-effects on the default buffer for the temp-table. This makes debugging difficult if an instance is evaluated via 'toString' in the debugger while the default-buffer is holding a record (and needs that record).
A pure-sql implementation of toString will decouple this from the default-buffer, and avoid side-effects.
#3 Updated by Stefan Brands about 1 year ago
Constantin Asofiei wrote:
Current implementation relies on WRITE-JSON, which has side-effects on the default buffer for the temp-table. This makes debugging difficult if an instance is evaluated via 'toString' in the debugger while the default-buffer is holding a record (and needs that record).
A pure-sql implementation of toString will decouple this from the default-buffer, and avoid side-effects.
Can we maybe raise the priority of this ? It is really giving us a hard time wile debugging...
#4 Updated by Stefan Brands about 1 year ago
Is this also why looking at the content of Exceptions changes the exception ? Because we just spend an entire week debugging a problem and in the end it turned out that the problem was not in the converted code but in our webui code. But we were mislead by the behavior in the debugger while looking at the exceptions.
I really think this kind of behavior in the debugger should be avoided and should be fixed. Otherwise a lot of our developers will run into this problem, especially when the entire company will switch to the converted code.
#5 Updated by Ovidiu Maxiniuc almost 1 year ago
Please use DataSet.getSqlData()
and RecordBuffer.sqlTableContent(N)
instead. At least as a temporarily workaround. They use SQL to extract the content from database so the buffer(s) is(are) not altered.
You may delegate the toString()
to these methods since the debuggers calls toString()
as default renderer. If your debugger supports data renderers (IntelliJ Idea does, but I am not sure about Eclipse) you may want to configure DataSet
and RecordBuffer
objects it to use them.
Important note:
In case of _temp
tables, the content is multiplexed, the table returned contain the records from ALL those tables. This was done intentionally at the moment the method was implemented. You need to use the _multiplex
field to filter only the record of interest.
#6 Updated by Constantin Asofiei almost 1 year ago
Ovidiu, that looks interesting; I think is better to have the legacy field name instead of column label or SQL field name (as for dynamic temp-tables, the SQL field name is like field#
).
Stefan: for object
FWD vars (legacy OO variables/fields in 4GL), the Java object.toString()
delegates to toLegacyString()
- which can be overridden by the 4GL class (as this is emitted for Progress.Lang.Object:toString()
). The point here: I'll disable this toString
to show only the object's actual type and maybe its internal ID. Otherwise, toLegacyString()
can have any implementation which is outside of FWD's control, and will have side-effects. If you want to drill-down into the object, you will need to look at the individual fields.
#7 Updated by Ovidiu Maxiniuc almost 1 year ago
Constantin Asofiei wrote:
The current result served a specific goal at the moment of the implementation (debugging a feature in development at that time). Usually, it is not difficult to identify the legacy name from their SQL counterpart. Except for dynamic temp-tables, where the columns are incremented values of a pattern (Ovidiu, that looks interesting; I think is better to have the legacy field name instead of column label or SQL field name (as for dynamic temp-tables, the SQL field name is like
field#
).
field#
). But we can improve the signature of the methods with additional parameters, beside limiting of the number of returned records we currently have:
- to use legacy column name (boolean);
- to include (or not) the hidden before-table attributes (boolean);
- to filter the multiplexed records to current buffer/table only (boolean or more complex);
- to crop or not longer character values (they are cropped now at 32 chars) (int);
- other (?)
However, since the toString()
has no parameters by default, we need to have default values for all above. And maybe define a resource (directory or file) to configure these values the developer needs at a certain moment.
#8 Updated by Alexandru Lungu 3 months ago
- Assignee set to Eduard Baciu
- Status changed from New to WIP
Ovidiu, are you aware if the WRITE-JSON
was finally rewritten without side-effects? Otherwise, Eduard B. can pick it up it fix it (for both trunk and 7156b).
#9 Updated by Ovidiu Maxiniuc 3 months ago
WRITE-JSON
will always leave the current/working buffer empty when it is finished, causing it content to be eventually flushed. This is documented in 4GL manual.
I guess you mean to rewrite toString()
method to avoid the side-effects by NOT invoking WRITE-JSON
runtime implementation?
I think the easiest way of inspecting a table/buffer SQL content is to call sqlTableContent(int)
on its RecordBuffer
, if any is present. For temp-tables, the default buffer can be used. The result is a tabular view of the SQL table.
If JSON is preferred, then it must be implemented. In fact, only the 'rendering' is necessary because the pulling of data from SQL should be identical.
Note the implementation of dataset
. Its toString()
behaviour can be switched at runtime (alter fullDebugString
field from debugger) to JSON/schema only. The JSON variant will eventually need to be "redirected" to use the buffer safe implementation. There is also already implemented the getSqlData()
which will return the tabular variant, but not wired to toString()
in any way.
Also, see #8183: I created a branch for a related purpose. My idea there was to extract debugging tools like this in a separate library which should not be packaged with the release binaries which get deployed to customers.
#10 Updated by Eduard Baciu 2 months ago
- % Done changed from 0 to 70
I rewrote toString function for StaticTempTable and TempTableBuilder using a StringBuilder in which i added elements(parenthesis and spaces, in addition to the information from the tables) in order to imitate the result returned by writeJson().
This is toString method from StaticTempTable which is almost identical with the one from TempTableBuilder:
public String toString() { String myName = (name == null) ? "unnamed" : name; StringBuffer jsonString = new StringBuffer("{\n \"" + myName + "\": ["); ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper(); boolean ws = eh.isSilent(); boolean wp = eh.isPending(); if (!ws) { eh.setSilent(true); } try { jsonString.append(defaultBuffer.buffer().sqlTableContentJson()); jsonString.append(" ]\n}"); } catch (PersistenceException | SQLException e) { jsonString.append("Error!"); } finally { if (!ws) { eh.setSilent(false); } if (!wp && eh.isPending()) { eh.clearPending(); } } return "STATIC " + myName + ", ID " + getUniqueID().getValue() + "\n" + jsonString; }
"sqlTableContentJson()" is a function from the RecordBuffer class written by me where the json with our informations is created.
public String sqlTableContentJson() throws PersistenceException, SQLException { String sql = "select * from " + dmoInfo.sqlTable; ResultSet res = persistence.getSession().getConnection().prepareStatement(sql).executeQuery(); ResultSetMetaData md = res.getMetaData(); int cc = md.getColumnCount(); StringBuilder resultSB = new StringBuilder(""); StringBuilder sb = new StringBuilder(""); while (res.next()) { sb.append(" {\n"); for (int k = 1; k <= cc; k++) { String label = md.getColumnLabel(k).toLowerCase(); if (!label.startsWith("_") && !label.equals("recid")) { String value = res.getString(k); sb.append(" \"" + label + "\":" + "\"" + value + "\",\n"); } } sb.replace(sb.length() - 2, sb.length() - 1, ""); sb.append(" }"); resultSB.append(sb + ","); sb.setLength(0); } resultSB.replace(resultSB.length() - 1, resultSB.length(), ""); return resultSB.toString(); }
I compared the results(WriteJson() and the current method ) and there are no differences, so now i will look at the DataSet part.
#11 Updated by Ovidiu Maxiniuc 2 months ago
- do not involve
ErrorManager
intoString()
: even if you use it in brackets to restore the state, this method must not alter application's internal state. Also it is not necessary, if an exception occurs, just display it instead of the JSON. It will be more useful for the developer which uses it. After all, this method should be used only when debugging/logging; - I see you skipped
!label.startsWith("_") && !label.equals("recid"))
"recid"
is the default value forSession.PK
, use that instead- I think these values are useful for the developers and should not be skipped. For example, when using a temp table, you may want to see record from a specific instance, but the query will return the content from all tables, multiplexed. In the absence of
_multiplex
field, this is not possible. Another example: when using before-tables, the links between the before/after images are maintained using_peerRowid
field and the record's PK. More than that, this make the the meta tables not viewable, and, although we do not encourage the customer to use fields whose name starts with underscore, they insist on doing that;
- when using
StringBuilder
:- try not to use constructs like:
sb.append(a + b)
. Usesb.append(a).append(b)
instead, to avoid an intermediaryStringBuilder
being constructed; - I see no point in using two
StringBuilder
instances. TheresultSB
can be used directly and drop additional operations for de/allocation ofsb
; - avoid
replace()
,delete()
as much as possible. Plan ahead. For example, in case of inserting comma between the elements of a list but not at the end, use something like this:boolean first = true; while (/*loop condition*/) { if (first) first = false; else sb.append(","); sb.append(/*next item in list*/) }
- try not to use constructs like:
#12 Updated by Eduard Baciu 2 months ago
- Status changed from WIP to Review
- % Done changed from 70 to 100
I modified the code following your advices and managed to implement "getDataAsJson" method for "DataSet" as well.
toString method for StaticTempTable:
public String toString() { String myName = (name == null) ? "unnamed" : name; StringBuffer jsonString = new StringBuffer("{\n \"" + myName + "\": ["); try { jsonString.append(defaultBuffer.buffer().sqlTableContentJson()); jsonString.append(" ]\n}"); } catch (PersistenceException | SQLException e) { jsonString.append("Error getting data: ").append(e.getMessage()).append("\n"); } return "STATIC " + myName + ", ID " + getUniqueID().getValue() + "\n" + jsonString; }
getDataAsJson method for DataSet:
private String getDataAsJson() { StringBuilder sb = new StringBuilder(); sb.append("{\n").append(" \"").append(name).append("\"").append(": {"); for (int i = 0; i < buffers.size(); i++) { try { BufferImpl buffer = buffers.get(i); String tableName = buffer.buffer().getDmoInfo().legacyTable; String res = buffer.buffer().sqlTableContentJson(); sb.append("\n").append(" \"").append(tableName).append("\": [").append(res); if (i == buffers.size() - 1) { sb.append(" ]\n"); } else sb.append(" ]").append(","); } catch (PersistenceException | SQLException e) { sb.append("Error getting data: ").append(e.getMessage()).append("\n"); } } sb.append(" }\n}"); return sb.toString(); }
sqlTableContentJson method from RecordBuffer:
public String sqlTableContentJson() throws PersistenceException, SQLException { String sql = "select * from " + dmoInfo.sqlTable; ResultSet res = persistence.getSession().getConnection().prepareStatement(sql).executeQuery(); ResultSetMetaData md = res.getMetaData(); int cc = md.getColumnCount(); StringBuilder resultSB = new StringBuilder(""); while (res.next()) { resultSB.append(" {\n"); for (int k = 1; k <= cc; k++) { String label = md.getColumnLabel(k).toLowerCase(); String value = res.getString(k); resultSB.append(" \"").append(label).append("\": " ).append("\"" ).append(value).append("\""); if (k != cc) { resultSB.append(",\n"); } else resultSB.append("\n"); } resultSB.append(" }"); if (!res.isLast()) { resultSB.append(","); } } return resultSB.toString(); } }
Thanks for the advices!
#13 Updated by Alexandru Lungu about 2 months ago
Ovidiu, please provide a second review for the changes.
#14 Updated by Ovidiu Maxiniuc about 2 months ago
- the overall implementation is good. Nearly all of the notes below are either to increase performance or to adhere to GCD coding style;
- please create a task branch for these changes;
StaticTempTable.getDataAsJson()
:- please use
StringBuilder
instead ofStringBuffer
. We observed an increase in performance with the builder. Even if that is minimal and this method is only used in debugging, let's be consequent across the project. - also, when using any of these classes, do not use string concatenation (
+
) in the append parameters. Instead use individual calls toappend()
method for each operand. The+
operator is a 'sugar syntax' and causes the compiler to create additionalStringBuilder
instances to be created behind the scenes; - the same goes at the final
return
; - in case of
TempTableBuilder
,defaultBuffer
may benull
if certain conditions.toString()
method will fail in that case. So this case should be intercepted and a message describing the situation (object not valid) returned;
- please use
DataSet.getDataAsJson()
- instead of
sb.append("{\n").append(" \"")
it's better to use a singlesb.append("{\n \"")
. There are 3 more similar occurrences; - please add curly braces to
else
branch. The coding style specifies this for the sake of uniformity and clarity;
- instead of
RecordBuffer.sqlTableContentJson()
:- please move
throws
on next line (I missed this in my previous review); - you can use the default
StringBuilder
constructor, instead of passing the empty string (""
); .append("\": " ).append("\"" )
should be replaced with.append("\": \"" )
;- please add curly braces to
else
branch; - this method will only be called from
toString()
methods of the classes from same package (com.goldencode.p2j.persist
). It makes sense to lower the visibility to package protected; - since all calls to this method will concatenate the result to a
StringBuilder
, it makes sense to receive it as parameter instead of creating an instance locally.
- please move
#15 Updated by Ovidiu Maxiniuc about 2 months ago
Here some additional notes on my #7473-14:
- please add descriptions for
@throws
tags in javadoc; - in
RecordBuffer.sqlTableContentJson()
, you need to close theResultSet
after iteration is over; - in same file, move the new method according to its visibility (after changing from
public
to package).
PS:
The trunk base revision is getting older, please rebase the branch.
#16 Updated by Andrei Iacob about 2 months ago
Rebased branch 7473a to trunk rev. 15208. Current revision at 15209.
#17 Updated by Eduard Baciu about 2 months ago
The modified version of
RecordBuffer.java - sqlTableContentJson
/** * Save the JSON representation of the content of a SQL table into a StringBuilder parameter. * * @throws PersistenceException * @throws SQLException * If any issue was encountered. Since this method is supposed to be called only in debug mode, * it's up to programmer to decide what went wrong. */ protected void sqlTableContentJson(StringBuilder resultString) throws PersistenceException, SQLException { String sql = "select * from " + dmoInfo.sqlTable; ResultSet res = persistence.getSession().getConnection().prepareStatement(sql) .executeQuery(); ResultSetMetaData md = res.getMetaData(); int cc = md.getColumnCount(); while (res.next()) { resultString.append(" {\n"); for (int k = 1; k <= cc; k++) { String label = md.getColumnLabel(k).toLowerCase(); String value = res.getString(k); resultString.append(" \"").append(label).append("\": \"").append(value).append("\""); if (k != cc) { resultString.append(",\n"); } else { resultString.append("\n"); } } resultString.append(" }"); if (!res.isLast()) { resultString.append(","); } } res.close(); }
StaticTempTable.java - toString()
public String toString() { String myName = (name == null) ? "unnamed" : name; StringBuilder jsonString = new StringBuilder("{\n \"" + myName + "\": ["); StringBuilder tableContentJson = new StringBuilder(""); StringBuilder resultString = new StringBuilder(""); try { defaultBuffer.buffer().sqlTableContentJson(tableContentJson); jsonString.append(tableContentJson); jsonString.append(" ]\n}"); } catch (PersistenceException | SQLException e) { jsonString.append("Error getting data: ").append(e.getMessage()).append("\n"); } return resultString.append("STATIC ").append(myName).append(", ID ") .append(getUniqueID().getValue()).append("\n").append(jsonString).toString(); }
TempTableBuilder.java - toString
public String toString() { if (!_prepared()) { // calling writeJson()n at this time will lead to NPE return "dynamic temp-table not yet prepared."; } if (defaultBuffer == null) { return "Default buffer is null"; } String myName = (name == null) ? "unnamed" : name().getValue(); StringBuilder jsonString = new StringBuilder("{\n \"" + myName + "\": ["); StringBuilder tableContentJson = new StringBuilder(""); StringBuilder resultString = new StringBuilder(""); try { defaultBuffer.buffer().sqlTableContentJson(tableContentJson); jsonString.append(tableContentJson); jsonString.append(" ]\n}"); } catch (PersistenceException | SQLException e) { jsonString.append("Error getting data: ").append(e.getMessage()).append("\n"); } return resultString.append("DYNAMIC ").append(myName).append(", ID ") .append(getUniqueID().getValue()).append("\n").append(jsonString).toString(); } }
DataSet - toString
private String getDataAsJson() { StringBuilder sb = new StringBuilder(); sb.append("{\n \"").append(name).append("\": {"); for (int i = 0; i < buffers.size(); i++) { try { BufferImpl buffer = buffers.get(i); String tableName = buffer.buffer().getDmoInfo().legacyTable; StringBuilder tableContentJson = new StringBuilder(""); buffer.buffer().sqlTableContentJson(tableContentJson); sb.append("\n \"").append(tableName).append("\": [").append(tableContentJson); if (i == buffers.size() - 1) { sb.append(" ]\n"); } else { sb.append(" ]").append(","); } } catch (PersistenceException | SQLException e) { sb.append("Error getting data: ").append(e.getMessage()).append("\n"); } } sb.append(" }\n}"); return sb.toString(); }
I hope it meets the requirements mentioned above.
#18 Updated by Ovidiu Maxiniuc about 1 month ago
Review of 7473a, r15210.
The majority of the issues from previous review were addressed, I have not seen any logic flaws in its logic. The notes bellow are mainly about local optimization:
RecordBuffer.sqlTableContentJson()
:resultString
parameter is missing from javadoc.DataSet.java
- line 7440:
StringBuilder
has a default constructor; you do not need to pass the empty string to this one; - line 7441/7442: the existence of
tableContentJson
is not necessary. These lines can be rewritten as:
so thatsb.append("\n \"").append(tableName).append("\": ["); buffer.buffer().sqlTableContentJson(sb);
sqlTableContentJson
will append directly tosb
. sb.append(" ]").append(",");
can be rewritten assb.append(" ],");
;
- line 7440:
StaticTempTable.toString()
:- mixing concatenation (
+
) withStringBuilder
APIs is not recommended (the compiler creates additionalStringBuilder
instances in the background). A construct likenew StringBuilder("{\n \"" + myName + "\": [")
should be replaced withnew StringBuilder("{\n \"").append(myName).append("\": [")
; - same as above too many
StringBuilder
instances. A single builder can handle the construction of the final result;
- mixing concatenation (
TempTableBuilder.toString()
- see above: too many instances of
StringBuilder
. You only have one (builder) lemming :).
- see above: too many instances of
Since there is a dedicated task branch, you do not need to post the new code in this task.
#19 Updated by Eduard Baciu about 1 month ago
I committed the changes.
#20 Updated by Greg Shah about 1 month ago
- Status changed from Review to Internal Test
What testing is needed?
#21 Updated by Alexandru Lungu about 1 month ago
I think the changes are only for debugging purposes, right? Ovidiu, please correct me if I am wrong. Despite trying it out and seeing that it works, I can't think of other means of testing.
#22 Updated by Ovidiu Maxiniuc about 1 month ago
Alexandru Lungu wrote:
I think the changes are only for debugging purposes, right? Ovidiu, please correct me if I am wrong. Despite trying it out and seeing that it works, I can't think of other means of testing.
Right, this is a debugging tool and should not be normally called from application code.
#23 Updated by Greg Shah about 1 month ago
- Status changed from Internal Test to Merge Pending
This can merge after 8001a.
#24 Updated by Greg Shah about 1 month ago
Since this hasn't merged yet, let's pause the merge so other branches can go.
Eduard: Let me know when you are available to merge it.
#25 Updated by Andrei Iacob about 1 month ago
This is ready to merge. Let us know when we can merge.
#26 Updated by Greg Shah about 1 month ago
You can merge to trunk now.
#27 Updated by Andrei Iacob about 1 month ago
- Status changed from Merge Pending to Test
Branch 7473a was merged to trunk rev 15236 and archived.