Skill Tracer
Skills › Scientific Agent Skills › omero-integration › tables.md

tables.md

275 lines · skills/omero-integration/references/tables.md at 5f2bc03

Open on GitHub
1# OMERO.tables
2
3OMERO.tables stores columnar analysis data as an `OriginalFile` backed by an
4HDF table. Treat table reads as data exports and table creation/update as
5server writes.
6

OMERO.tables

OMERO.tables stores columnar analysis data as an OriginalFile backed by an HDF table. Treat table reads as data exports and table creation/update as server writes.

Raw lines 1 to 6
7## Compatibility and Scope
8
9The current stable OMERO.tables definition is part of `omero-blitz` 5.8.5 in
10the OMERO.server 5.6.18 documentation. OMERO.server 5.6.12 specifically
11required OMERO.py 5.19.4 for the Tables service to start correctly, which
12illustrates why client/server pairing matters.
14Before using tables:
16- confirm the server's tested OMERO.py version;
17- confirm the Tables service is active;
18- select one group and one table/attached object;
19- cap columns and rows;
20- plan handle closure even after read/query failure.

Compatibility and Scope

The current stable OMERO.tables definition is part of omero-blitz 5.8.5 in the OMERO.server 5.6.18 documentation. OMERO.server 5.6.12 specifically required OMERO.py 5.19.4 for the Tables service to start correctly, which illustrates why client/server pairing matters.

Before using tables:

  • confirm the server's tested OMERO.py version;
  • confirm the Tables service is active;
  • select one group and one table/attached object;
  • cap columns and rows;
  • plan handle closure even after read/query failure.
Raw lines 7 to 21
22## Column Types
24Current scalar columns:
26- `FileColumn`, `ImageColumn`, `RoiColumn`, `WellColumn`, `PlateColumn`
27- `BoolColumn`
28- `LongColumn` (signed 64-bit)
29- `DoubleColumn` (64-bit)
30- `StringColumn(name, description, size, values)`
32Current fixed-width array columns include:
34- `FloatArrayColumn(name, description, size, values)`
35- `DoubleArrayColumn(name, description, size, values)`
36- `LongArrayColumn(name, description, size, values)`
38Upstream still labels array columns experimental. The array `size` argument
39is required. Older examples that omit it are stale.
40All columns added in one operation must contain the same number of rows.
41Column names are unique; names beginning with double underscore are reserved.
43The service performs limited validation of string and array lengths. Validate
44every value against the initialized schema before writing.

Column Types

Current scalar columns:

  • FileColumn, ImageColumn, RoiColumn, WellColumn, PlateColumn
  • BoolColumn
  • LongColumn (signed 64-bit)
  • DoubleColumn (64-bit)
  • StringColumn(name, description, size, values)

Current fixed-width array columns include:

  • FloatArrayColumn(name, description, size, values)
  • DoubleArrayColumn(name, description, size, values)
  • LongArrayColumn(name, description, size, values)

Upstream still labels array columns experimental. The array size argument is required. Older examples that omit it are stale. All columns added in one operation must contain the same number of rows. Column names are unique; names beginning with double underscore are reserved.

The service performs limited validation of string and array lengths. Validate every value against the initialized schema before writing.

Raw lines 22 to 45
46## Read-Only Table Inspection
48Start from one explicit `OriginalFile` ID, not a filename search:
50```python
51original_file_id = 123
52max_rows = 100
53max_columns = 20
55original = conn.getObject("OriginalFile", original_file_id)
56if original is None:
57 raise LookupError("Table OriginalFile unavailable")
59resources = conn.c.sf.sharedResources(conn.SERVICE_OPTS)
60table = resources.openTable(original._obj, conn.SERVICE_OPTS)
61try:
62 headers = list(table.getHeaders())
63 if len(headers) > max_columns:
64 raise ValueError("Table has more columns than approved")
66 row_count = table.getNumberOfRows()
67 stop = min(row_count, max_rows)
68 column_indices = list(range(len(headers)))
69 data = table.read(column_indices, 0, stop) if stop > 0 else None
71 print(
72 {
73 "original_file_id": original_file_id,
74 "total_rows": row_count,
75 "returned_rows": stop,
76 "truncated": row_count > stop,
77 "columns": [column.name for column in headers],
78 }
79 )
80finally:
81 table.close()
82```
84`read(colNumbers, start, stop)` uses a stop-exclusive row range, except the
85current docs note that `start=0, stop=0` returns the first row. Avoid that edge
86case: do not call `read` when the approved row count is zero.
88Other current read methods:
90- `readCoordinates(rowNumbers)`: complete rows at explicit indices
91- `slice(colNumbers, rowNumbers)`: selected columns and rows
92- `getWhereList(condition, variables, start, stop, step)`: matching row
93 indices, which can then be passed to `readCoordinates`
95An empty column or row selection in `slice` may mean “all,” so never use empty
96lists as a safety limit.

Read-Only Table Inspection

Start from one explicit OriginalFile ID, not a filename search:

original_file_id = 123
max_rows = 100
max_columns = 20

original = conn.getObject("OriginalFile", original_file_id)
if original is None:
    raise LookupError("Table OriginalFile unavailable")

resources = conn.c.sf.sharedResources(conn.SERVICE_OPTS)
table = resources.openTable(original._obj, conn.SERVICE_OPTS)
try:
    headers = list(table.getHeaders())
    if len(headers) > max_columns:
        raise ValueError("Table has more columns than approved")

    row_count = table.getNumberOfRows()
    stop = min(row_count, max_rows)
    column_indices = list(range(len(headers)))
    data = table.read(column_indices, 0, stop) if stop > 0 else None

    print(
        {
            "original_file_id": original_file_id,
            "total_rows": row_count,
            "returned_rows": stop,
            "truncated": row_count > stop,
            "columns": [column.name for column in headers],
        }
    )
finally:
    table.close()

read(colNumbers, start, stop) uses a stop-exclusive row range, except the current docs note that start=0, stop=0 returns the first row. Avoid that edge case: do not call read when the approved row count is zero.

Other current read methods:

  • readCoordinates(rowNumbers): complete rows at explicit indices
  • slice(colNumbers, rowNumbers): selected columns and rows
  • getWhereList(condition, variables, start, stop, step): matching row indices, which can then be passed to readCoordinates

An empty column or row selection in slice may mean “all,” so never use empty lists as a safety limit.

Raw lines 46 to 97
98## Paged Read
100Read consecutive chunks instead of every row:
102```python
103def iter_table_pages(table, column_indices, *, limit=1000, page_size=100):
104 if not 1 <= limit <= 10_000:
105 raise ValueError("limit out of range")
106 if not 1 <= page_size <= min(limit, 500):
107 raise ValueError("page_size out of range")
109 total = min(table.getNumberOfRows(), limit)
110 start = 0
111 while start < total:
112 stop = min(start + page_size, total)
113 yield table.read(column_indices, start, stop)
114 start = stop
115```
117Do not accumulate all pages unless the total cap and memory cost were reviewed.
118Large string/array columns can make a small row count expensive.

Paged Read

Read consecutive chunks instead of every row:

def iter_table_pages(table, column_indices, *, limit=1000, page_size=100):
    if not 1 <= limit <= 10_000:
        raise ValueError("limit out of range")
    if not 1 <= page_size <= min(limit, 500):
        raise ValueError("page_size out of range")

    total = min(table.getNumberOfRows(), limit)
    start = 0
    while start < total:
        stop = min(start + page_size, total)
        yield table.read(column_indices, start, stop)
        start = stop

Do not accumulate all pages unless the total cap and memory cost were reviewed. Large string/array columns can make a small row count expensive.

Raw lines 98 to 119
120## Fixed Queries and Bound Variables
122OMERO.tables uses PyTables condition syntax. Keep the condition code fixed and
123bind user values:
125```python
126from omero.rtypes import rlong
128row_count = table.getNumberOfRows()
129max_rows_considered = min(row_count, 1000)
130matches = []
131if max_rows_considered > 0:
132 matches = list(table.getWhereList(
133 condition="(Image > minimum_id)",
134 variables={"minimum_id": rlong(100)},
135 start=0,
136 stop=max_rows_considered,
137 step=0,
138 )[:100])
139data = table.readCoordinates(matches) if matches else None
140```
142Never concatenate user text into a condition. Validate column names against
143`getHeaders()` and map requested operations to a fixed allowlist.
145The documented condition language includes logical, comparison, arithmetic,
146and selected mathematical operations. Treat it as a query language, not as
147arbitrary Python. Do not use Python `eval()` or `exec()` around it.

Fixed Queries and Bound Variables

OMERO.tables uses PyTables condition syntax. Keep the condition code fixed and bind user values:

from omero.rtypes import rlong

row_count = table.getNumberOfRows()
max_rows_considered = min(row_count, 1000)
matches = []
if max_rows_considered > 0:
    matches = list(table.getWhereList(
        condition="(Image > minimum_id)",
        variables={"minimum_id": rlong(100)},
        start=0,
        stop=max_rows_considered,
        step=0,
    )[:100])
data = table.readCoordinates(matches) if matches else None

Never concatenate user text into a condition. Validate column names against getHeaders() and map requested operations to a fixed allowlist.

The documented condition language includes logical, comparison, arithmetic, and selected mathematical operations. Treat it as a query language, not as arbitrary Python. Do not use Python eval() or exec() around it.

Raw lines 120 to 148
149## Creating a Table
151Creation is a write. Use a unique, caller-reviewed name and one selected
152repository:
154```python
155from omero.grid import DoubleColumn, ImageColumn, StringColumn
157columns = [
158 ImageColumn("Image", "Source image", []),
159 DoubleColumn("MeanIntensity", "Mean raw value", []),
160 StringColumn("Status", "Review status", 32, []),
163resources = conn.c.sf.sharedResources(conn.SERVICE_OPTS)
164repositories = resources.repositories(conn.SERVICE_OPTS).descriptions
165if not repositories:
166 raise RuntimeError("No table repository is available")
168repository_id = repositories[0].getId().getValue()
169table = resources.newTable(
170 repository_id, "analysis-v2-explicit-name", conn.SERVICE_OPTS,
172try:
173 table.initialize(columns)
174 table.addData(
175 [
176 ImageColumn("Image", "Source image", [101, 102]),
177 DoubleColumn("MeanIntensity", "Mean raw value", [12.5, 14.0]),
178 StringColumn("Status", "Review status", 32, ["reviewed", "reviewed"]),
179 ]
180 )
181 table_file_id = table.getOriginalFile().getId().getValue()
182finally:
183 table.close()
184```
186Before execution:
188- verify repository selection with the administrator;
189- validate every referenced object ID in the same intended group;
190- validate all row counts, numeric ranges, strings, and array sizes;
191- cap rows per `addData` batch;
192- decide recovery if initialization succeeds but data upload fails.

Creating a Table

Creation is a write. Use a unique, caller-reviewed name and one selected repository:

from omero.grid import DoubleColumn, ImageColumn, StringColumn

columns = [
    ImageColumn("Image", "Source image", []),
    DoubleColumn("MeanIntensity", "Mean raw value", []),
    StringColumn("Status", "Review status", 32, []),
]

resources = conn.c.sf.sharedResources(conn.SERVICE_OPTS)
repositories = resources.repositories(conn.SERVICE_OPTS).descriptions
if not repositories:
    raise RuntimeError("No table repository is available")

repository_id = repositories[0].getId().getValue()
table = resources.newTable(
    repository_id, "analysis-v2-explicit-name", conn.SERVICE_OPTS,
)
try:
    table.initialize(columns)
    table.addData(
        [
            ImageColumn("Image", "Source image", [101, 102]),
            DoubleColumn("MeanIntensity", "Mean raw value", [12.5, 14.0]),
            StringColumn("Status", "Review status", 32, ["reviewed", "reviewed"]),
        ]
    )
    table_file_id = table.getOriginalFile().getId().getValue()
finally:
    table.close()

Before execution:

  • verify repository selection with the administrator;
  • validate every referenced object ID in the same intended group;
  • validate all row counts, numeric ranges, strings, and array sizes;
  • cap rows per addData batch;
  • decide recovery if initialization succeeds but data upload fails.
Raw lines 149 to 193
194## Linking a Table
196The table `OriginalFile` becomes discoverable through a `FileAnnotation` and an
197object-annotation link:
199```python
200from omero.model import (
201 DatasetAnnotationLinkI,
202 DatasetI,
203 FileAnnotationI,
204 OriginalFileI,
207file_annotation = FileAnnotationI()
208file_annotation.setFile(OriginalFileI(table_file_id, False))
209file_annotation = conn.getUpdateService().saveAndReturnObject(
210 file_annotation, conn.SERVICE_OPTS,
213link = DatasetAnnotationLinkI()
214link.setParent(DatasetI(dataset_id, False))
215link.setChild(FileAnnotationI(file_annotation.getId().getValue(), False))
216conn.getUpdateService().saveAndReturnObject(link, conn.SERVICE_OPTS)
217```
219This is a second write after table creation. If link creation fails, the table
220and file annotation may remain orphaned. Record IDs after every successful
221step and define cleanup before starting.
223OMERO.web table viewing expects conventions such as a column named `Image`
224with a supported ID/numeric column type. A custom column called something else
225may store valid data but not receive the same UI behavior.

Linking a Table

The table OriginalFile becomes discoverable through a FileAnnotation and an object-annotation link:

from omero.model import (
    DatasetAnnotationLinkI,
    DatasetI,
    FileAnnotationI,
    OriginalFileI,
)

file_annotation = FileAnnotationI()
file_annotation.setFile(OriginalFileI(table_file_id, False))
file_annotation = conn.getUpdateService().saveAndReturnObject(
    file_annotation, conn.SERVICE_OPTS,
)

link = DatasetAnnotationLinkI()
link.setParent(DatasetI(dataset_id, False))
link.setChild(FileAnnotationI(file_annotation.getId().getValue(), False))
conn.getUpdateService().saveAndReturnObject(link, conn.SERVICE_OPTS)

This is a second write after table creation. If link creation fails, the table and file annotation may remain orphaned. Record IDs after every successful step and define cleanup before starting.

OMERO.web table viewing expects conventions such as a column named Image with a supported ID/numeric column type. A custom column called something else may store valid data but not receive the same UI behavior.

Raw lines 194 to 226
227## Concurrency and Lifecycle
229Each OMERO table is backed by one HDF table. PyTables/HDF does not support
230general concurrent access, so OMERO.tables adds global locking. Keep table
231handles short-lived:
233```python
234table = resources.openTable(original._obj, conn.SERVICE_OPTS)
235try:
236 # One bounded operation.
237 ...
238finally:
239 table.close()
240```
242Do not hold a handle across:
244- user interaction;
245- network retries/reconnection;
246- long pixel analysis;
247- another process's expected write window.
249If BlitzGateway reconnects, discard any old table proxy and open a fresh one.

Concurrency and Lifecycle

Each OMERO table is backed by one HDF table. PyTables/HDF does not support general concurrent access, so OMERO.tables adds global locking. Keep table handles short-lived:

table = resources.openTable(original._obj, conn.SERVICE_OPTS)
try:
    # One bounded operation.
    ...
finally:
    table.close()

Do not hold a handle across:

  • user interaction;
  • network retries/reconnection;
  • long pixel analysis;
  • another process's expected write window.

If BlitzGateway reconnects, discard any old table proxy and open a fresh one.

Raw lines 227 to 250
251## Updating or Deleting
253`addData()` and `update()` mutate table content. Deleting the backing
254`OriginalFile` may remove the table; deleting one annotation link may merely
255detach it from one object. These require separate impact review.
257Never:
259- update rows selected by a dynamically constructed condition;
260- append an unbounded result set;
261- delete a table based only on filename;
262- overwrite an existing table as a retry strategy;
263- leave a handle open after an exception.

Updating or Deleting

addData() and update() mutate table content. Deleting the backing OriginalFile may remove the table; deleting one annotation link may merely detach it from one object. These require separate impact review.

Never:

  • update rows selected by a dynamically constructed condition;
  • append an unbounded result set;
  • delete a table based only on filename;
  • overwrite an existing table as a retry strategy;
  • leave a handle open after an exception.
Raw lines 251 to 264
265## Table Checklist
267- Explicit `OriginalFile` ID and selected group
268- Server/client pairing verified
269- Column count, row limit, page size, string size, and array width bounded
270- Fixed conditions with bound variables
271- No empty selection that means “all”
272- Handle closed in `finally`
273- Writes and links reviewed separately
274- Partial-create IDs recorded for recovery
275- Export classification reviewed before JSON/CSV output

Table Checklist

  • Explicit OriginalFile ID and selected group
  • Server/client pairing verified
  • Column count, row limit, page size, string size, and array width bounded
  • Fixed conditions with bound variables
  • No empty selection that means “all”
  • Handle closed in finally
  • Writes and links reviewed separately
  • Partial-create IDs recorded for recovery
  • Export classification reviewed before JSON/CSV output
Raw lines 265 to 275