Showing posts with label admin. Show all posts
Showing posts with label admin. Show all posts

Thursday, May 19, 2011

How to understand those integrity checker error codes

One of the things I like most about travelling to conferences is seeing the new and innovative applications that are available for Smallworld users. One such innovation that particularly caught my interest was the Data Quality Toolkit by Realworld ECHO. This tool picks up where the core Smallworld integrity checker leaves off and provides a better interface for identifying data quality issues in the database. If you have ever been overwhelmed by the cryptic output of the integrity checker, then you owe it to yourself to review this product.

A few years ago, a PNI customer asked me to review his PNI integrity checker log files and make recommendations about ignore/fix for each error type. It turns out that there was absolutely no documentation available for understanding the error codes. The only way I could understand the codes was to use global text searches or even print_users_of() to find the place where the code was raised and then try to deduce what the actual data condition was. Well, that was good for me as a billable consultant, but not so good for the customer.

If they had had a tool like the Data Quality Toolkit, this problem would not have been as acute. What I find particularly nice about the Data Quality Toolkit is the associated QCDB Knowledge Database. This is essentially an on-line wiki that has been keyed to those cryptic error codes. So now, when your integrity checking process encounters an error code, the Data Quality Toolkit gives you a link to the appropriate web page in the Knowledge Database which provides you with a detailed explanation of causes and fixes for that error type. And because this wiki is a living document, if you ever encounter an error code that has not yet been documented, let Realworld ECHO know and they will update the relevant page. The nice thing about that is future users of the service will now automagikally have access to the same analysis. No need to wait for a TSB to be issued with updated documentation. What we have here is a slick use of online tools to improve the data quality of the entire PNI/LNI/Office community.

Good job folks! Keep up the good work.

Wednesday, November 17, 2010

Visualization in Smallworld (Part 2)

Back in January, I posted an article about Visualization in Smallworld. The article was long on words and short on visuals, so now I have created a video that shows more clearly what the post about.

Friday, October 15, 2010

Decimal Degrees vs DMS

I had someone ask me today how to configure Smallworld to show Decimal Degrees or Degrees/Minutes/Seconds.

I found two ways. The one way is to use the Administration application's Unit Display Groups tool to change the "Sub Units?" value



The other option is to create some Magik code in your image build process that changes the sub units setting.

E.g.
_block
unit_manager.unit(:degree).default_format.use_sub_units? << _false # (or _true)
_endblock
$

Wednesday, August 25, 2010

Where was that method or exemplar defined?

Do you ever want to know the source file for a particular method? You can use the class browser to jump to the code or you can do this...

MagikSF> circle.method(:new|()|).source_file
$

Most of the time, jumping to a method is faster in the Class Browser than using this code snippet.

But what if you want to jump to the file where the actual class exemplar (not the method) is defined? Then you can do this...

MagikSF> circle.method(:exemplar).source_file
$

I know of no way to do this latter action from the Class Browser.

Tuesday, August 17, 2010

How much does an alternative contribute to file size?

If you want determine how much a particular alternative contributes to the overall size of a .ds file, use these debug methods...


_block
_local v << gis_program_manager.cached_dataset(:gis)

v.dump_file_usage(v.file_usage())
_endblock
$


See the public comments for ds_version_view.file_usage() for a description of the data that is returned. Based on that, you can figure out what percentage of total blocks used is attributable to a particular alternative.

This can give you guidance for which alternatives to merge/post/delete, etc.

Tuesday, April 27, 2010

Debugging batch files

Creating Smallworld images often requires one or more batch files with myriads of environment variables. This link gives a good summary of various batch file debugging techniques.

[NOTE: a reader notified me that the referenced link is broken. You can find a cached version of it at Google here]

Monday, January 18, 2010

Visualization in Smallworld

(the cool stuff/demo is at the end of this longer-than-normal post)

Lately I have taken an interest in Visualization techniques within the Smallworld context. I am using the term "Visualization" in the sense of using visual representations to simplify complex data relationships. The term has various other related meanings as well.

In a sense, we work with Visualization techniques in Smallworld every day... we deal with a visualization of geographic relationships between objects that are of interest to us. When you create a report using the Schematics Generator you are using visualization techniques to help you comprehend complex relationships by keeping only the salient information. Any Thematic Mapping tool that you might use also lets you visualize complex data relationships easier.

I have been intrigued by the use of heat maps to provide a way of showing intensity of some attribute spatially. I still haven't got my mind around all the applications that might be used in the utility space for heat maps. Some that come to mind are: heat map of all the transformers that are about to blow up; heat map of all the outages in a storm. While the pictures look cool, I'm trying to better understand what value a heatmap image would bring to GIS users that cannot be already provided by other kinds of thematic mapping applications.

I have a feeling that the full potential for visualizations such as heat maps and density maps has not been realized within the Smallworld realm but I sure could use some ideas from readers about what possible applications might be.

My colleague Graham Garlick has done alot of work with generating heat maps, density maps and TIN density maps within Smallworld and I want to put a plug in for him in this post. If you are interested in knowing more about how to generate these kinds of maps in Smallworld, please contact iFactor Consulting and ask for Graham's heat map demo.

So now to the final point of this post. The visualizations that I have already referred to are typically georeferenced rasters that overlay on a map and give you a sense of the spatial relationships of your data. I have been exploring another area that involves network visualizations.

My first foray into this realm was by making use of a software package called Graphviz to visually show the results of network follower trace results in PNI. This was really helpful in debugging network follower results. Typically, a Magik developer debugs network follower trace results by querying the nf_links of a network follower and trying to understand how they are linked. Using a visualization tool like Graphviz allowed me to easily create a .jpg file that I could view in any image viewer. By looking at the image output, I could easily spot places where the trace stopped unexpectedly or went down an unexpected branch. Sometimes a picture really is worth a thousand words (or a thousand minutes of Magik debugging!!!)

My second attempt at network visualization came from a slightly different tack. If visualization techniques are meant to represent complex interdependencies in an easy-to-understand manner, what is a really complex difficult-to-understand scenario in Smallworld that I could try to visualize? The answer to that question is: XML configuration file dependencies. Anyone that has ever tried to modify config.xml, gui.xml or a host of other XML configuration files in Smallworld will tell you that it can be somewhat mystifying as to which version of the file to change. Any given XML file can inherit from another XML file and its elements can supersede the same elements of a "superclass" XML file. In addition to that, XML files are considered module resource files. That means that they can be placed in customisation products which have a different pathname than the same module found in the core product. If this all sounds confusing, rest assured it is. I have seen many instances (and caused some myself) where I have changed the wrong XML file or changed too many "inheriting" XML files when it would have been more efficient to change a "superclass" XML file. But it was always difficult to get my mind wrapped around the big picture of XML file dependencies. Until now...

I have take a bit of time to write a Magik wrapper around the Graphviz engine. That in itself is not that remarkable and anyone with interest in that could do it. What is of more interest to me is the result that I was able to create with this Graphviz tool showing the interdependencies of XML files. As an example, click here to see the dependencies of the Electric Office config.xml files for applications.

  • If you look at the image, you will see information about which XML files inherit from others.
  • You will see the full pathname of each file. This will help you know whether to look in the actual module resource file or in a corresponding customization product file
  • You will see some cells that are green. These are the cells that represent XML file elements that have a material change relative to a "superclass" file.
  • You will see some cells that are red. These are cells that represent XML elements that exist in a configuration file but whose contents are no different than the same element in the inherited XML file. Many times it can be confusing to have the exact same XML element contents in two files, so looking at this visualization gives you an easy-to-understand representation of which duplicate XML elements you can remove.
  • You will see some cells that are yellow. These represent XML elements that require further investigation. They indicate that the current XML element is an exact duplicate of another XML element somewhere in the graph. If you notice yellow cells in two XML files that share the same parent, you might want to consider moving those XML elements up the inheritance ladder.
  • And finally, just seeing the overall flow of XML inheritance can give you a quick indication of whether you have too many dependencies or maybe where it would make the most sense to insert a suitable XML file "superclass" that would serve your own customizations most efficiently.
This was just a quick demonstration of how a network visualization tool can be used to simplify complex interdependencies in the Smallworld environment. I would love to hear from readers about other complex scenarios that might lend themselves to this sort of network visualization.

Monday, October 19, 2009

Version Viewer

My colleague Graham Garlick has been working on a very cool Version Viewer application. It is essentially a GUI that allows you to view/compare two different versions of a dataset. It lets you compare geometries and attributes and determine what has changed between the two versions. Imagine Smallworld's conflict detector's Affected Objects Lister but the Version Viewer is easy for you to use in non-conflict situations.

What could you use the Version Viewer for?
- recovering attributes/records from data that is available in an older view
- analysing your data BEFORE a merge/post. You could build this into a pre-emptive conflict avoidance process
- selective merge/post by attribute
- data quality checks to see the who/what/when of changes in your data. Very helpful for investigating the inevitable question: "How/why did that data get changed and in what context were the changes made".

Graham also is responsible for the Dialog Designer. He used the Dialog Designer to build the complex Version Viewer GUI. As part of the Version Viewer demo, he can show you how you can dynamically change the GUI layout of that application using Dialog Designer.\

Please come by the iFactor Consulting booth at the conference and ask for a demo by Graham. If you are not able to make it to the booth, sent me a note at the contact information on the blog sidebar and I can arrange a demo.

Wednesday, October 14, 2009

Getting more from tracebacks

Now that I've spammed my readers with a useless "test post via email" note, I will try to post something of value.

I have always been fond of saying "tracebacks are our friends". In my mind, tracebacks are not to be feared... the bigger the traceback the better because that means that I have more information available to me to debug the problem.

A common debugging technique is to put a call to the !traceback!() procedure in any method from which you want to determine how the call stack arrived at that method. Placing a !traceback!() call in your code won't raise a condition or stop the code flow. It will simply give you a dump of the call stack at that point.

To demonstrate how this works, I have put a !traceback!() call in the Web Map Connector's ve_tile.draw_on() method. The traceback looks as you would expect...

---- traceback: renderer (heavy_thread 3611775) ----
time=14/10/2009 17:00:21
sw!version=4.1.0 (swaf)
os_text_encoding=cp1252
!snapshot_traceback?!=False

!traceback!()
a ve_tile(detached) .draw_on(a sw:canvas)
sw:simple_vector:[1-11].fast_keys_and_elements()
sw:keyed_set(1).valid_indices(False)
sw:keyed_set(1).fast_elements()
sw:keyed_set(1).elements()
transient_rwo_collection(road_tile).fast_elements()
sw:simple_vector:[1-11].fast_keys_and_elements()
sw:hash_table(1).valid_indices(False)
sw:hash_table(1).fast_elements()
ve_dataset(ve_dataset).scan_elements(a bounding_box, sw:rope:[1-1], sw:hash_table(0))
a sw:uds_geometry_set(ve_dataset,0,0).scan_elements(a bounding_box, sw:rope:[1-1], sw:hash_table(0))
a sw:uds_geometry_set(ve_dataset,0,0).elements()
a sw:uds_geometry_set(ve_dataset,0,0).draw_on(a sw:canvas, unset)
sw:rope:[1-4].elements()
a sw:composite_geometry_set.draw_on(a sw:canvas, unset)
a sw:canvas.paint(True, a sw:composite_geometry_set, unset)
a sw:canvas.draw(a sw:composite_geometry_set)
map_view(map(Electricity),region).super(map_view).original!int!do_render(unset)
map_view(map(Electricity),region).int!do_render(unset)
renderer(map_view(map(Electricity),region), unset)
light_thread_launcher_proc_990928()


There is a global variable !snapshot_traceback?! that you set to _true to give you a bit more information.

MagikSF> !snapshot_traceback?! << _true
$


Now the traceback looks like...

---- traceback: renderer (heavy_thread 3563153) ----
time=14/10/2009 17:01:44
sw!version=4.1.0 (swaf)
os_text_encoding=cp1252
!snapshot_traceback?!=True

[1] !traceback!()
[2] a ve_tile(detached) .draw_on(a sw:canvas)
[3] sw:simple_vector:[1-11].fast_keys_and_elements()
[4] sw:keyed_set(1).valid_indices(False)
[5] sw:keyed_set(1).fast_elements()
[6] sw:keyed_set(1).elements()
[7] transient_rwo_collection(road_tile).fast_elements()
[8] sw:simple_vector:[1-11].fast_keys_and_elements()
[9] sw:hash_table(1).valid_indices(False)
[10] sw:hash_table(1).fast_elements()
[11] ve_dataset(ve_dataset).scan_elements(a bounding_box, sw:rope:[1-1], sw:hash_table(0))
[12] a sw:uds_geometry_set(ve_dataset,0,0).scan_elements(a bounding_box, sw:rope:[1-1], sw:hash_table(0))
[13] a sw:uds_geometry_set(ve_dataset,0,0).elements()
[14] a sw:uds_geometry_set(ve_dataset,0,0).draw_on(a sw:canvas, unset)
[15] sw:rope:[1-4].elements()
[16] a sw:composite_geometry_set.draw_on(a sw:canvas, unset)
[17] a sw:canvas.paint(True, a sw:composite_geometry_set, unset)
[18] a sw:canvas.draw(a sw:composite_geometry_set)
[19] map_view(map(Electricity),region).super(map_view).original!int!do_render(unset)
[20] map_view(map(Electricity),region).int!do_render(unset)
[21] renderer(map_view(map(Electricity),region), unset)
[22] light_thread_launcher_proc_990928()



... as you can see, each traceback line is numbered. This is kind of handy to have. The real benefits of !snapshot_traceback?! are described in the public comments for this global variable.

What I wanted to show you is a modification I made to the !traceback!() procedure that gives you even more helpful information whilst !snapshot_traceback?! is _true.

Here is my hack of the !traceback!() procedure.

#% text_encoding = iso8859_1
_package sw
$
_global !traceback! <<
_proc@traceback(_optional stream, max_depth, full?)
## Display traceback of calls in the current thread on
## STREAM. If max_depth is given, at most that number of
## stack frames will be displayed.
##
## If !traceback_show_args?! is true, then the arguments will
## be shown along with the method name.
##
## Tracebacks normally exclude stack frames after a
## condition.raise(). Setting FULL? to _true will force the
## full traceback to be shown. I.e including tracebacks of any
## condition handlers.

_thisthread.traceback(stream, !traceback_show_args?!,max_depth, full?)

_global !snapshot_traceback?!

_if !snapshot_traceback?! _is _true
_then

_global !ts!
_local sfile , meth

write("These numbered source files correspond to the methods in the numbered lines in the traceback above.")

_for i,v _over !ts!.fast_keys_and_elements()
_loop
meth << v.receiver.method(v.message)

_if meth _is _unset
_then
show(write_string("[",i,"]"))
_continue
_endif

sfile << meth.source_file
_dynamic !show_string_length! << sfile.size
show(write_string("[",i,"]"),sfile)
_endloop
_endif
_endproc
$



You can see that it calls the standard _thisthread.traceback() method, but after that it takes a list of all the methods/procedures that were reported in the traceback and shows their source files as well...


---- traceback: renderer (heavy_thread 3514507) ----
time=14/10/2009 17:05:41
sw!version=4.1.0 (swaf)
os_text_encoding=cp1252
!snapshot_traceback?!=True

[1] traceback()
[2] a ve_tile(detached) .draw_on(a sw:canvas)
[3] sw:simple_vector:[1-11].fast_keys_and_elements()
[4] sw:keyed_set(1).valid_indices(False)
[5] sw:keyed_set(1).fast_elements()
[6] sw:keyed_set(1).elements()
[7] transient_rwo_collection(road_tile).fast_elements()
[8] sw:simple_vector:[1-11].fast_keys_and_elements()
[9] sw:hash_table(1).valid_indices(False)
[10] sw:hash_table(1).fast_elements()
[11] ve_dataset(ve_dataset).scan_elements(a bounding_box, sw:rope:[1-1], sw:hash_table(0))
[12] a sw:uds_geometry_set(ve_dataset,0,0).scan_elements(a bounding_box, sw:rope:[1-1], sw:hash_table(0))
[13] a sw:uds_geometry_set(ve_dataset,0,0).elements()
[14] a sw:uds_geometry_set(ve_dataset,0,0).draw_on(a sw:canvas, unset)
[15] sw:rope:[1-4].elements()
[16] a sw:composite_geometry_set.draw_on(a sw:canvas, unset)
[17] a sw:canvas.paint(True, a sw:composite_geometry_set, unset)
[18] a sw:canvas.draw(a sw:composite_geometry_set)
[19] map_view(map(Electricity),region).super(map_view).original!int!do_render(unset)
[20] map_view(map(Electricity),region).int!do_render(unset)
[21] renderer(map_view(map(Electricity),region), unset)
[22] light_thread_launcher_proc_990928()
These numbered source files correspond to the methods in the numbered lines in the traceback above.
"[1]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/general/procedure.magik"
"[2]" "C:\DOCUME~1\Alfred\LOCALS~1\Temp\TAlfred9412q0"
"[3]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/mixins/ro_indexed_collection_mixin.magik"
"[4]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/new_sys_changes.magik"
"[5]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/convert2_sf/hashing/hash_values_helper.magik"
"[6]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/new_sys_changes.magik"
"[7]" "c:/swdev/build/410/410_PB_WINDOWS/unshipped/source/sys_core/ds_src/dd/dd_collection_mixin.magik"
"[8]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/mixins/ro_indexed_collection_mixin.magik"
"[9]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/new_sys_changes.magik"
"[10]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/convert2_sf/hashing/hash_values_helper.magik"
"[11]" "C:/ifactor/products/workspace/Virtual_Earth_Connector/trunk/modules/ve_som/source\ve_dataset.magik"
"[12]" "C:/Smallworld41/product/modules/sw_core/geometry_vector/source/uds_geometry_set.magik"
"[13]" "C:/Smallworld41/product/modules/sw_core/geometry_vector/source/uds_geometry_set.magik"
"[14]" "C:/Smallworld41/product/modules/sw_core/geometry_vector/source/geometry_set_mixin.magik"
"[15]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/mixins/rope_mixin.magik"
"[16]" "C:/Smallworld41/product/modules/sw_core/geometry_vector/source/composite_geometry_set.magik"
"[17]" "c:/swdev/build/410/410_PB_WINDOWS/unshipped/source/sys_core/mgr_src/user/drawing_surface_mixin.magik"
"[18]" "c:/swdev/build/410/410_PB_WINDOWS/unshipped/source/sys_core/mgr_src/user/drawing_surface_mixin.magik"
"[19]" "C:\ifactor\products\workspace\Virtual_Earth_Connector\trunk\source\patches_z_proposed\p1000001_1_if_vec_map_plugin.magik"
"[20]" "C:\ifactor\products\workspace\Virtual_Earth_Connector\trunk\source\patches_z_proposed\p1000001_1_if_vec_map_plugin.magik"
"[21]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/general/procedure.magik"
"[22]" "/swdev/streams/leo/release/unshipped/source/sys_core/sys_src/general/procedure.magik"


This can be very helpful when you are trying to debug why certain code works in one of your environments but not the other. It can be very helpful to the GE Help Desk team because sending this enhanced traceback will tell them how your call stack is different than the one they are testing in. It will show which methods were changed in patches and in custom code.

Monday, October 5, 2009

Preventing ACE posting

It has long been known that posting up ACE and/or CASE alternatives may have unintended consequences when the parent/child alternatives have records that are in conflict. If you ever want to prevent all users from posting an ACE alternative, you can use this code I wrote to do that.

This code snippet has been written in a patch format. You can read more about how to create your own patches here.
The define_method_synonym() approach is described here. Note that because the core method database_view.post() source code is not shipped with the product, we need to use the define_method_synonym() approach to wrap our own custom functionality around it without modifying the core restricted functionality.


#% text_encoding = iso8859_1
_package sw
$
sw!patch_software( "ds_src", 1 )
$
# use define_method_synonym() to wrap some of our behavior
# around the core method without redefining the core behavior.
_if database_view.method(:original!post|()|) _is _unset
_then
database_view.define_method_synonym(:original!post|()|,:post|()|)
_endif
$
_pragma(classify_level=restricted, usage={redefinable})
_method database_view.post(_gather args)
## Post changes in this alternative to the parent,
## performing an implicit commit() if any updates are
## outstanding.
##
## Returns _true if there were changes to post, _false
## otherwise.
##
## If the data dictionary has been modified, then the modified
## version must have been allocated a datamodel version first.
##
## External databases are checked to ensure that if they are
## updated, that they are prepared to commit (i.e. they are
## running). An error is raised if one is not prepared to
## commit.
##
## THIS REDEFINED VERSION OF THE METHOD HAS BEEN MODIFIED TO
## DISALLOW POSTING ACE ALTERNATIVES.

_if _self.name.canonical _is :ace
_then
condition.raise(:cannot_post_ace_database)
_endif
_return _self.original!post(_scatter args)
_endmethod
$
_pragma(classify_level=restricted, usage={redefinable})
## raised by database_view.post() when database_view is named
## :ace.
condition.define_condition(:cannot_post_ace_database,:user_error,{})
$
read_message_patch(:condition,_unset,:en_us)
$
:cannot_post_ace_database Posting ACE alternatives is not safe and has been disabled.
$

sw!declare_patch(1000000001,"1cst","Prevents posting ACE alternative.")
$

Wednesday, February 6, 2008

CASE Tool out of synch with your actual datamodel

It is possible that your CASE Tool representation of your datamodel might not accurately reflect the actual datamodel you have in your database. This happened to me recently when I discovered that some CASE work I had done in my own development environment had touched more objects than I was working with when I did a CASE Archive. When I loaded the CASE archive into the production database I had not noticed that it also updated some other objects with an older version of the CASE definition.

This was not a problem until someone tried to apply changes for the "unknowingly-touched" CASE object and it introduced some confusion into the production environment. Well, we resolved the issue but then had the question... How do we ensure that the CASE Tool accurately represents the state of the datamodel?

The answer to that question: "Reverse Engineer"

Here are some tips that will help you to reverse engineer a CASE model.
  • read the Smallworld Case Tutorial help "Reverse engineering from the Gis dataset"
  • an important thing to note is that you need to do the reverse engineer action into an empty CASE partition. Use a procedure such as create_database() to create new "empty" case.ds and case_dd.ds files and then update your Case SOC to point to these new .ds files
  • you should also temporarily keep your "old" case.ds and case_dd.ds files. Do this by creating a new SOC (:old_case) that has a :case dataset that points to these old ds files.
  • when you perform the reverse engineer action you will see that the graphics for relationships and drafting_* objects are not visible in the new CASE. You will also see that the CASE objects are not grouped together the way you had them in the original CASE tool. That is because the Gis dataset does not keep information about CASE graphics in its data dictionary.
  • I use a script like the following one to "recover" some of my description geometries from the "old" CASE tool into the new recently-reverse-engineered CASE tool. (Be sure that you have a handle to both the old and new CASE tools for this).

_local new_case_v << gis_program_manager.cached_dataset(:case)
_local old_case_v << gis_program_manager.cached_dataset(:old_case)

_local old_drafting_coll, new_drafting_coll , geometries , properties , new_rec

write("creating CASE drafting_* objects...")
_for drafting_table_name _over {:drafting_lines,:drafting_areas,:drafting_points,:drafting_texts}.fast_elements()
_loop
old_drafting_coll << old_case_v.collections[drafting_table_name]
new_drafting_coll << new_case_v.collections[drafting_table_name]

_for old_rec _over old_drafting_coll.fast_elements()
_loop
properties << property_list.new()

new_rec << new_drafting_coll.insert(old_rec)

_for a_geom _over old_rec.geometries().fast_elements()
_loop
properties[a_geom.app_type] << a_geom
_endloop

_if properties.empty?
_then
_continue
_endif

record_transaction.new_update(new_rec,properties).run()
_endloop
_endloop

_local new_case_coll << new_case_v.collections[:sw_gis!case_object]
_local old_case_coll << old_case_v.collections[:sw_gis!case_object]

write("updating CASE Object geometries...")
_for new_co _over new_case_coll.fast_elements()
_loop
_if (old_co << old_case_coll.select(predicate.eq(:name,new_co.name)).an_element()) _is _unset
_then
show("skipping ",new_co.name)
_continue
_endif
show(old_co,new_co)

record_transaction.new_update(new_co,property_list.new_with(:outline,old_co.outline,
:position,old_co.position)).run()
_endloop

_local new_case_coll << new_case_v.collections[:sw_gis!case_relationship]
_local old_case_coll << old_case_v.collections[:sw_gis!case_relationship]

write("redrawing CASE relationship geometries...")
_for new_co _over new_case_coll.fast_elements()
_loop
_if (old_co << old_case_coll.select(predicate.eq(:object_info,new_co.object_info)).an_element()) _is _unset
_then
show("skipping ",new_co.object_info)
_continue
_endif

record_transaction.new_update(new_co,property_list.new_with(:centreline,old_co.centreline,
:end1,old_co.end1,
:end2,old_co.end2,
:label,old_co.label)).run()
_endloop

Wednesday, October 31, 2007

Identify and Recover Deleted Data without a database restore

Customers will come to us and say... "someone deleted/updated some data a while back and we only discovered it now. We don't want to restore a backup because that will undo the work from the last week. Help!"

It may be possible to "rescue" the old data if you still have a suitably older checkpoint somewhere in your database.

First, you need to get a full inventory of all the checkpoints available to you. It is possible that there exist checkpoints in your database that you do not even know about. The following code will give you a full inventory and list the checkpoints (and alternative that they are in) sorted from most recent to oldest creation times.

_pragma(classify_level=debug)
_method ds_version_view.debug!all_checkpoints()
 ## debug!all_checkpoints() : sorted_collection
 ##
 ## returns a sorted_collection of all checkpoints in self and
 ## the tree beneath self sorted from most recent to oldest.
 ## The elements returned are actually simple_vectors of the
 ## form:
 ##       {checkpoint A_CHECKPOINT,string ALTERNATIVE_NAME}
 ##
 _local checkpoints << sorted_collection.new(_unset,
          _proc(a,b)
           >> a[1].time_created > b[1].time_created
          _endproc)
 
 _for a_view _over _self.actual_alternatives()
 _loop
  checkpoints.add_all(a_view.checkpoints.map(_proc(a_cpt)
           _import a_view
           >> {a_cpt,a_view.alternative_path_name()}
          _endproc)) 
 _endloop


 _return checkpoints
_endmethod
$
_pragma(classify_level=debug)
_global show_all_checkpoints <<
_proc@show_all_checkpoints(a_view)
 ## show_all_checkpoints(ds_version_view A_VIEW) : _unset
 ##
 ## shows all checkpoints and related information for A_VIEW and
 ## tree.

 _for cpt_info _over a_view.debug!all_checkpoints().fast_elements()
 _loop
  show(cpt_info[1].time_created,cpt_info[1].checkpoint_name,cpt_info[2])
 _endloop
_endproc
$


To run this code, do something like...

MagikSF> show_all_checkpoints(gis_program_manager.cached_dataset(:gis))
$


... which will give you output like...

date_time(10/30/07 17:34:44) "is!insync_1193790807" :||oracle_sync_parent|
date_time(10/30/07 17:34:44) "is!insync_1193790807" :||oracle_sync_parent|oracle_sync|
date_time(10/30/07 16:32:54) "is!insync_1193787116" :||oracle_sync_parent|
date_time(10/30/07 16:32:53) "is!insync_1193787116" :||oracle_sync_parent|oracle_sync|
date_time(10/30/07 15:33:09) "is!insync_1193783570" :||oracle_sync_parent|
date_time(10/30/07 15:33:08) "is!insync_1193783570" :||oracle_sync_parent|oracle_sync|
date_time(10/30/07 14:34:02) "is!insync_1193780009" :||oracle_sync_parent|
date_time(10/30/07 14:34:02) "is!insync_1193780009" :||oracle_sync_parent|oracle_sync|
date_time(10/30/07 13:33:52) "is!insync_1193776407" :||oracle_sync_parent|


Once you have identified a checkpoint that is old enough to contain your old data, you take one of the two following two approaches (comments in the code explain the differences).

_block
 # if you want to use your GIS tools to see what the data looked
 # like at a previous checkpoint, then get a handle on the
 # current view and go that checkpoint in READONLY mode.

 # DO NOT switch to :writable while at this alternative unless
 # you are ABSOLUTELY SURE of what you are doing.  The safest
 # action to take once you are done viewing data at the old
 # checkpoint is to switch to the "***disk version***"
 # checkpoint in that alternative before continuing.
 _local v << gis_program_manager.cached_dataset(:gis)
 
 v.switch(:readonly)
 v.go_to_alternative("|oracle_sync_parent",:readonly)
 v.go_to_checkpoint("is!insync_1192840224",:readonly)
_endblock
$

... OR ...
_block
 # if you want to keep the main dataset at the current view and
 # then get a second handle to the old_view so that you can copy
 # old data (eg., incorrectly deleted/updated data) into the
 # current view, then get a handle on a REPLICA of the current
 # view and go that previous checkpoint in READONLY mode.

 # DO NOT switch to :writable while at this alternative unless
 # you are ABSOLUTELY SURE of what you are doing.  The safest
 # action to take once you are done viewing data at the old
 # checkpoint is to switch to the "***disk version***"
 # checkpoint in that alternative before continuing.
 _global old_view

 # once you have finished using OLD_VIEW, be sure to :discard()
 # it.
 old_view << gis_program_manager.cached_dataset(:gis).replicate()
 
 old_view.switch(:readonly)
 old_view.go_to_alternative("|oracle_sync_parent",:readonly)
 old_view.go_to_checkpoint("is!insync_1192840224",:readonly)

_endblock
$


If you are going to use data in the old_view to update/recreate data in the current view, then you might want to explore using classes/methods such as
record_transaction.new_update()
record_transaction.new_insert()
ds_collection.at()
ds_collection.insert()
ds_collection.clone_record()
ds_collection.update()
ds_collection.insert_or_update()



[NOTE 2014 June 02: Reinhard Hahn sent me some new code that supercedes the code he originally posted in the comment to this post]

_pragma(classify_level=debug)
_iter _method ds_version_view.all_available_versions(_optional top?)
  ## yields all available Versions of a replicate of
  ## _self in the following order:
  ##
  ## alternatives (if TOP? isnt _false: beginning with ***top***)
  ## -- checkpoints (beginning with :current)
  ## -- base versions for checkpoints (only for alternative_level
  ##    > 0)
  ##
  ## for each version yields
  ## A_VIEW,ALTERNATIVE_PATH_NAME,CHECKPOINT_NAME,BASE_LEVEL, CHECKPOINTS_TIME_CREATED
  ## where A_VIEW is always a replicate, which will be discarded
  ## outside this method. The order of the CHECKPOINTS will be in
  ## reversed chronological order (i.e. beginning with the most
  ## recent one)
  ##
  ## BASE_LEVEL is 0 for the version itself, -1 for the immediate
  ## base version, -2 for the base version of the base version
  ## and so on 
  ##
  ## example:
  ## v,:|***top***|,:current,0,v.time_last_updated()
  ## v,:|***top***|,"cp_1",0,cp_1.time_created
  ## v,:|***top***|,"cp_2",0,cp_2.time_created
  ## v,:||alt1|,:current,0,v.time_last_updated()
  ## v,:||alt1|,:current,-1,v.time_last_updated()
  ## v,:||alt1|,"cp11",0,cp11.time_created
  ## v,:||alt1|,"cp11",-1,v.time_last_updated()
  ## v,:||alt2|,":current",0,v.time_last_updated()
  ## ...
  ##
  ## NB:
  ## If the different versions of _self have different
  ## datamodels, you might be annoyed by messages like the following:
  ##
  ##  compiliing my_table.geometry_mapping
  ##  fixing up details for dd_field_type(my_field_type) : Version 12345, transition to 54321
  ##  Defining my_other_table
  ##
  ## To avoid this, you might want to suppress any output by
  ## redefining the dynamics !output! and suppressing the output
  ## of the information conditions db_synchronisation and
  ## pragma_monitor_info. Of course you have then to put any
  ## wanted output to other output targets like !terminal! or a
  ## locally defined external_text_output_stream. So your code
  ## might look like the following: 
  ## MagikSF> t << proc(a_database_view)
  ##  _handling db_synchronisation, pragma_monitor_info _with procedure
  ##  _dynamic !output!  << null_text_output_stream
  ##
  ##  # ... any more preparation code ...
  ##
  ##  _for r,alt_pname,cp,base_level,cp_time _over a_database_view.all_available_versions()
  ##  _loop
  ##
  ##    # ... my code running in the loop, for example:
  ##    !terminal!.show(alt_pname,cp,base_level,cp_time)
  ##    !terminal!.write(%newline)
  ##    !terminal!.flush()
  ##    # ...
  ##
  ##  _endloop
  ##
  ##  # ... any summary output, for example:
  ##  !terminal!.write("Ready.",%newline)
  ##  !terminal!.flush()
  ##  # ...
  ##
  ## _endproc.fork_at(thread.low_background_priority)
  ##
  ## If you don't want to use these !terminal! calls, you could
  ## also dynamicall redefine !output!:
  ## MagikSF> t << proc(a_database_view)
  ##  _handling db_synchronisation, pragma_monitor_info _with procedure
  ##  _dynamic !output!  << null_text_output_stream
  ##
  ##  # ... any more preparation code ...
  ##
  ##  _for r,alt_pname,cp,base_level,cp_time _over a_database_view.all_available_versions()
  ##  _loop
  ##    _block
  ##      _dynamic !output! << !terminal!
  ##      # ... my code running in the loop, for example:
  ##      show(alt_pname,cp,base_level,cp_time)
  ##      # ...
  ##    _endblock
  ##  _endloop
  ##
  ##  !output! << !terminal!
  ##  # ... any summary output, for example:
  ##  write("Ready.")
  ##
  ## _endproc.fork_at(thread.low_background_priority)

  _local change_alt_level <<
    _if top? _isnt _false 
    _then
      >> - _self.alternative_level
    _else
      >> 0
    _endif
  _local r << _self.replicate(change_alt_level,_true)
  _protect
    _for rr _over r.actual_alternatives(:same_view?,_true)
    _loop
      _local alt_name << rr.alternative_path_name()
#      _if alt_name.write_string.matches?("*{consistency_validation}") _then _continue _endif
      _local cps << rr.checkpoints.as_sorted_collection(_proc(lhs,rhs)
                      _if lhs _is :current
                      _then
                        _if rhs _is :current
                        _then
                          _return _maybe
                        _else
                          _return _true
                        _endif
                      _elif rhs _is :current
                      _then
                        _return _false
                      _endif 
                      _local res <<  rhs.time_created _cf lhs.time_created
                      _if res _isnt _maybe
                      _then
                        _return res
                      _endif
                      _return lhs.checkpoint_name _cf rhs.checkpoint_name
                    _endproc)
      cps.add(:current)
      _for cp _over cps.fast_elements()
      _loop
        _local cp_name
        _local cp_time 
        _if cp _is :current
        _then 
          rr.rollforward()
          cp_name << :current
          cp_time << rr.time_last_updated()
        _else
          cp_name << cp.checkpoint_name
          rr.go_to_checkpoint(cp_name)
          cp_time << cp.time_created
        _endif 
        _loopbody(rr,alt_name,cp_name,0,cp_time)
        _if rr.alternative_level > 0
        _then
          _local rrr << rr.replicate(:base,_true)
          _protect
            _local level_diff << -1
            _loopbody(rrr,alt_name,cp_name,level_diff,rrr.time_last_updated())
            _loop
              _if rrr.alternative_level = 0 _then _leave _endif
              rrr.up_base()
              level_diff -<< 1
              _loopbody(rrr,alt_name,cp_name,level_diff,rrr.time_last_updated())
            _endloop 
          _protection
            rrr.discard()
          _endprotect
        _endif 
      _endloop
    _endloop
  _protection
    r.discard()
  _endprotect
_endmethod
$

Wednesday, October 3, 2007

MUnit Testing Framework

I recently received a request from a reader wondering where the unit test application was on my blog. I don't have a post on that yet so I thought that now was as good a time as any to make one.

I'm not going to go into the details here. Suffice it to say, that the MUnit framework is based on the original JUnit framework but for Magik instead of Java.

I have found it helpful when developing code to create some kind of automated test scripts. There is much that can be debated about how much or how little test scripting to do.

Mark Field had a detailed presentation at the Smallworld 2007 Americas Users Conference this year titled "Implementing MUnit for Smallworld". I imagine you could contact him for the presentation or maybe we can get him to post more information on his blog. If you have the username/password, you can download the presentation from https://www.kinsleyassociates.com/ge/smallworld/presentations.asp

[NOTE: The above links are old. You can find the presentation slides and download zip file at http://sw-gis.wikidot.com/magik-munit]

GE also had/has a version of MUnit but from what I understand they are reluctant to release it to the community. I found that there was a version of MUnit created by Jan Vorel at http://www.janvorel.com. Unfortunately the current version of that site is not very user friendly like it used to be and you can no longer download Jan's code from there. But if you look at http://web.archive.org/web/20030406030601/www.janvorel.com/ you will find an archived version of the web site that still has a working link to his version of MUnit. It only works on CST 3.3 but I found that with a few modifications it works in CST 4.x as well.

The implementations of MUnit that I have seen do not allow you to test mouse/keyboard input. Mark's presentation describes how he enhanced MUnit to include those features.

One enhancement I did for the MUnit framework some time ago was to create a new test listener that exported the test results as XML and/or HTML. An example of those results is here.

You can click here for a Yahoo Groups search result on MUnit.

Thursday, September 6, 2007

ACEs, ACEs, everywhere

I recently tried to make a copy of an ACE in the Physical Network Inventory (PNI) product. The out-of-the-box ACE is called "Physical NI User". So I thought that all I needed to do was make a copy of that ACE, call it something like "Physical NI User New", and then modify the application registration code to include...

:ace_name,"Physical NI User New"


Apparently that was not enough. If you want to have two user groups use different ACEs, you definitely need to change the application registration :ace_name. But you also need to change the ACE name in the following XML configuration files:

in a suitable application config.xml file
  • verify config/framework_data/map_type[@ace_name] has the name you are expecting
  • verify config/plugins/plugin[@class_name="pni_editor_manager"]/property[@name="ace_name"]["value"] has the name you are expecting
  • verify config/plugins/plugin[@class_name="document_manager"]/document/document_properties/property[@name="ace_name"]["value"] has the name you are expecting
In addition, if you have specified any bookmarks for an ACE, they are "hardcoded" for the ACE in which they were originally created.

The typical symptom which indicates that some of the above-mentioned ACE settings are incorrect is the one where you look at the object control plugin and notice that the v/h/s settings are not what you thought you had set them to in the current ACE. Most likely you did set the v/h/s settings correctly but one or more of your application configurations are still pointing to an ACE that you are not expecting it to.

Wednesday, July 11, 2007

how to exhaust your UVAs without knowing it

Unique Value Allocators (UVAs) in Smallworld are special objects that return a unique integer value for system IDs only once. Theoretically you could exhaust your UVA bucket at some point and then you will not be able to insert any more RWOs, geometries, links, nodes and other system objects.

There are a number of ways to exhaust your UVAs but one that came to my attention recently is the method :copy_transform() on various geometry classes. I have used this method in the past for exporting geometries to some other file format. What was strange at the time was that when I used this method I received a "database not writable" error. That raised some red flags in my mind: why does the database need to be writable if all I intend to do is read data from it and export it to another file format?

It turns out in that case that I was using :copy_transform() to convert the coordinates of geometries from one coordinate system to another. Unbeknown to me, the :copy_transform() method actually makes a copy of your geometry before transforming it. So you are left with two copies of the same geometry in the database after the transform. And creating a copy of the geometry uses UVAs (which cannot be recovered) and involves more system resources than you would likely need for your intended purpose -- simply to transform the coordinates of a geometry from one coordinate system to another.

I am writing all this because we have come across a situation where an installation used export code from another vendor that apparently makes frequent use of the :copy_transform() method to export geometry data into a 3rd party format. Recently that installation's main UVA was exhausted. There is nothing wrong with transforming your coordinates before exporting to a 3rd party but there are simpler ways of doing transforms using !current_coordinate_system! that do not gobble up UVA resources (maybe I will post about that some day).

I would recommend that you run the following command at the Magik prompt:

MagikSF> print_users_of(:copy_transform|()|)
$

The output of this procedure will list all methods that call :copy_transform(). Scan the list and if any of the method names (or classes that they are written on) look as though they are not core Smallworld methods/classes, please confirm that that code cannot be done some other way without using up your precious UVAs.


(There are other ways to exhaust your UVAs prematurely, but I wanted to focus on this one approach today)

Tuesday, April 10, 2007

getting a list of all users currently connected to Smallworld

I learned a new aspect of Windows command-line scripting the other day that I thought might be worth sharing here.

The requirement was to get a list of all users connected to a particular swmfs server and notify them that the server was about to go down.

I discovered that the FOR command is just what the doctor ordered for this task. The basic form is as follows...

FOR /F "skip=5 delims=@" %%i IN ('swmfs_list -full \\%COMPUTERNAME%\sw400\product\data message.ds') DO net send %%i Smallworld server going down in 2 minutes.


Where...

  • FOR /F ... IN ...
    • Command key words that tell the system that you want to iteratively process the results of some file or command
  • ('swmfs_list -full \\%COMPUTERNAME%\sw400\product\data message.ds')
    • the command that will have each line processed individually for some information. This particular command provides a list of all the user@computername that are connected to message.ds in the specified folder on %COMPUTERNAME%
  • %%i
    • the internal variable representing each line of the file/command
  • "skip=5 delims=@"
    • instructions to the command to skip the first 5 lines of swmfs_list and then also split the processed lines by the @ character.
  • DO
    • a command key word that indicates that what follows next is the command to be processed on each line of iterator
  • net send %%i Smallworld server going down in 2 minutes.
    • the NET SEND command is used to send the message "Smallworld server going down in 2 minutes" to the user specified in variable %%i. Remember that the variable %%i represents the first element of each line after it has been split by the @ character.

Special note about the variables. If you are running this script from within a BAT file, you need to reference the variable with two percent signs: eg., %%i. If you want to test this functionality at the command line, you need to reference the variable with only one percent sign: eg., %i.

I hope you find this useful.

getting a list of all users currently connected to Smallworld

I learned a new aspect of Windows command-line scripting the other day that I thought might be worth sharing here.

The requirement was to get a list of all users connected to a particular swmfs server and notify them that the server was about to go down.

I discovered that the FOR command is just what the doctor ordered for this task. The basic form is as follows...

FOR /F "skip=5 delims=@" %%i IN ('swmfs_list -full \\%COMPUTERNAME%\sw400\product\data message.ds') DO net send %%i Smallworld server going down in 2 minutes.


Where...

  • FOR /F ... IN ...
    • Command key words that tell the system that you want to iteratively process the results of some file or command
  • ('swmfs_list -full \\%COMPUTERNAME%\sw400\product\data message.ds')
    • the command that will have each line processed individually for some information. This particular command provides a list of all the user@computername that are connected to message.ds in the specified folder on %COMPUTERNAME%
  • %%i
    • the internal variable representing each line of the file/command
  • "skip=5 delims=@"
    • instructions to the command to skip the first 5 lines of swmfs_list and then also split the processed lines by the @ character.
  • DO
    • a command key word that indicates that what follows next is the command to be processed on each line of iterator
  • net send %%i Smallworld server going down in 2 minutes.
    • the NET SEND command is used to send the message "Smallworld server going down in 2 minutes" to the user specified in variable %%i. Remember that the variable %%i represents the first element of each line after it has been split by the @ character.
I hope you find this useful.

Wednesday, March 7, 2007

North American Daylight Saving Time

(Thank you to everyone that posted comments in response to the announcement that GE will be introducing a new version of Smallworld on Oracle/Java.)

I'm not sure about Mexico, but Canada and USA switched to Daylight Saving Time on March 11. If your Smallworld system times are off by an hour, please read "USA/Canada Daylight Saving Time is 4 weeks longer this year. Are you ready?"

You can tell if you have the appropriate Smallworld patch loaded by executing the following command at the Magik prompt.

MagikSF> date_time_now()
$


If the date and time are different than you expect, it is likely that your patch has not been loaded.

An e-mail from the Smallworld helpdesk also mentioned that the most recent time zone patches include adjustments for Western Australia.


Friday, February 23, 2007

ds_transfer might be corrupting your backups

If you are using the ds_transfer function in "hot" mode to create backups from your production VMDS, you run the risk of corrupting your backup files so that they cannot be properly restored. I had originally published a post about this to the sw-gis group in July of last year. Recently I was at a client site where the backup scheme used ds_transfer in "hot" mode and I realized that it would be helpful to repost this information in this blog.

If you are using PowerOn, it is possible that the original implementors set up your backup scheme to use ds_transfer in "hot" mode. You may want to read the following article and see if your backups are in jeopardy.

Original Article follows....

Since there is a discussion about backups and the ds_transfer mechanism was mentioned as a solution I thought I would throw out a warning about that approach. Simply put, when using ds_transfer.new() in "hot" mode, you should ensure that no one is writing to the database while you are performing the ds_transfer.
The typical ds_transfer scenario involves ds_transferring one DS file at a time. Imagine that you ds_transfer gdb.ds and it takes a total of 15 minutes. 10 minutes into the ds_transfer a user writes a fiber(1234) RWO and geometry to that partition. But the alternative that was being written to had already been processed at minute 2 of the 15 minute process. After the 15 minutes are done, the ds_transfer starts working on the rwo.ds file.
You will not see a problem in the live production dataset, but you will now have a situation where the "ds_transferred" dataset has an inconsistency between the rwo.ds and the gdb.ds. This is a simple scenario, but imagine that changes are at a larger scale or possibly involve adding/removing alternatives while a hot ds_transfer is in progress. These will all be permitted actions but may cause unintended results in the "ds_transferred" files. If you are using the ds_transfer.new() with "hot" mode you should try opening a copy of your recently ds-transferred files to see if they are consistent with each other.
If you want to continue using the ds_transfer functionality to perform "hot" backups, refer to ds_transfer.transfer_partition() to see how it might be beneficial to you. This method has the advantage of allowing you to snapshot all DS files in a partition at the same time so you will not encounter any of these data consistency issues.