Synapse Tables enable storage of tabular data in Synapse in a form that can be queried using a SQL-like query language.
A table has a schema and holds a set of rows conforming to that
schema. A schema defines a series of Columns of the
following types: STRING, DOUBLE,
INTEGER, BOOLEAN, DATE,
ENTITYID, FILEHANDLEID, LINK,
LARGETEXT, MEDIUMTEXT, USERID,
SUBMISSIONID, EVALUATIONID, JSON,
STRING_LIST, INTEGER_LIST,
BOOLEAN_LIST, DATE_LIST,
ENTITYID_LIST, USERID_LIST.
In this vignette you will:
Project
tutorial.First, let’s load some data. Let’s say we had a file, genes.csv:
| Name | Chromosome | Start | End | Strand | TranscriptionFactor |
|---|---|---|---|---|---|
| foo | 1 | 12345 | 12600 | + | False |
| arg | 2 | 20001 | 20200 | + | False |
| zap | 2 | 30033 | 30999 | - | False |
| bah | 1 | 40444 | 41444 | - | False |
| bnk | 1 | 51234 | 54567 | + | True |
| xyz | 1 | 61234 | 68686 | + | False |
synBuildTable().table <- Table(
name = "My Favorite Genes_no_initial_columns",
parent_id = project$id
) |> synStore()
table |> synStoreRows(
values = genes_csv,
schema_storage_strategy = "INFER_FROM_DATA"
)synStoreRows() accepts data from several sources:
A string holding the path to a CSV file. If the
schema_storage_strategy is set to NULL the
data will be uploaded as is. If schema_storage_strategy is
set to INFER_FROM_DATA, the code makes assumptions about
the format of the columns in the CSV.
A named list where each name is a column name and each value is one or more values.
An R data frame.
Creating a table from a CSV with specified columns
columns <- list(
Column(name = "Name", column_type = "STRING", maximum_size = 20),
Column(name = "Chromosome", column_type = "STRING", maximum_size = 20),
Column(name = "Start", column_type = "INTEGER"),
Column(name = "End", column_type = "INTEGER"),
Column(name = "Strand", column_type = "STRING", enum_values = list("+", "-"), maximum_size = 1),
Column(name = "TranscriptionFactor", column_type = "BOOLEAN")
)
table <- Table(
name = "My Favorite Genes_with_columns",
columns = columns,
parent_id = project$id
) |> synStore()
table |> synStoreRows(values = genes_csv)genes <- tibble::tibble(
Name = c("foo", "arg", "zap", "bah", "bnk", "xyz"),
Chromosome = c("1", "2", "2", "1", "1", "1"),
Start = c(12345L, 20001L, 30033L, 40444L, 51234L, 61234L),
End = c(12600L, 20200L, 30999L, 41444L, 54567L, 68686L),
Strand = c("+", "+", "-", "-", "+", "+"),
TranscriptionFactor = c(FALSE, FALSE, FALSE, FALSE, TRUE, FALSE)
)
table <- Table(
name = "My Favorite Genes_from_dataframe",
parent_id = project$id
) |> synStore()
table |> synStoreRows(values = genes, schema_storage_strategy = "INFER_FROM_DATA")The query language is quite similar to SQL select statements, except that joins are not supported. The documentation for the Synapse API has lots of query examples. Let’s try to query:
results <- synQuery(sprintf("SELECT * FROM %s WHERE Chromosome = '1' AND Start < 41000 AND \"End\" > 20000", table$id))
resultsThe synQuery() will always be returned as a data frame
unless you specify a download_location in which case the
results will be downloaded to that location. There are a number of
arguments that you may pass to this function depending on if you are
getting the results back as a DataFrame or downloading the results to a
file.
Once the schema is settled, changes come in two flavors: appending
new rows and updating existing ones. One other piece of information
required for making changes to tables is the etag, which is
used by the Synapse server to prevent concurrent users from making
conflicting changes through a technique called optimistic concurrency.
This comes as a result of running synQuery. In the example
above, you could see the etag by running results$etag - but
you should never have to use it directly. In case of a conflict, your
update may be rejected. You then have to do another query and try your
update again.
Appending new rows is fairly straightforward. To continue the
previous example, we might add some new genes from another file. Lets
take this new data in more_genes.csv.
The more_genes.csv:
| Name | Chromosome | Start | End | Strand | TranscriptionFactor |
|---|---|---|---|---|---|
| Qux1 | 4 | 201001 | 202001 | + | False |
| Qux2 | 4 | 203001 | 204001 | + | False |
more_genes_csv <- tempfile(fileext = ".csv")
writeLines(
c(
"Name,Chromosome,Start,End,Strand,TranscriptionFactor",
"Qux1,4,201001,202001,+,False",
"Qux2,4,203001,204001,+,False"
),
more_genes_csv
)
table <- Table(
name = "My Favorite Genes_from_dataframe",
parent_id = project$id
) |> synGet()
table |> synStoreRows(values = more_genes_csv)NA values
are added automatically.Updating the existing table requires querying the rows you want to
change first, since Synapse needs the ROW_ID and
ROW_VERSION for each row being modified —
synQuery() includes these columns by default. Minimizing
changesets to contain only rows that actually change will make
processing faster.
For example, let’s update the names of some of our favorite genes:
results <- synQuery(sprintf("SELECT * FROM %s WHERE Chromosome = '1'", table$id))
results$Name <- c("rzing", "zing1", "zing2", "zing3")
table |> synStoreRows(values = results)Now, query the table again to see your changes:
If your data has a primary key, you can use the upsert functionality
(synUpsertRows) to update existing rows or insert new rows
based on this primary key. This way you won’t have to query for the
existing data to update your table. This function does not do
deletions.
toUpsert <- tibble::tibble(
Name = c("Qux3", "Qux5"),
Chromosome = c("4", "4"),
Start = c(201001L, 203001L),
End = c(202001L, 204001L),
Strand = c("-", "+"),
TranscriptionFactor = c(TRUE, FALSE)
)
# Qux3 will be updated, Qux5 will be inserted
table |> synUpsertRows(values = toUpsert, primary_keys = list("Name"))Column
object accessed through columns, then storing the table
again:table$columns[["Expression"]]$name <- "Expression2"
table$columns[["Expression"]]$column_type <- "INTEGER"
table |> synStore()Deleting specific rows — query for the rows you want to delete and
call synDeleteRows() on the results:
Rows can also be deleted based on a data.frame, where the
ROW_ID and ROW_VERSION columns specify the
rows to be deleted from the table. In this example, rows 2 and 3 are
deleted. See this document that describes the expected columns: https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/Row.html.
Note: ROW_VERSION begins at 1 upon row creation and
increases by one with every subsequent update.
Deleting the whole table deletes the table and all of its rows:
Synapse DATE columns store an exact moment in time,
represented as the number of milliseconds since
1970-01-01 00:00:00 UTC. Before upload, the client converts
the datetime values in your data to that representation, and how a value
is converted depends on whether it carries timezone information. The
examples below create a small table with a DATE column and
walk through the different ways datetime data can be stored and
queried.
A timezone-aware datetime is converted to UTC exactly — the stored
value does not depend on the timezone settings of the machine performing
the upload or on the date the upload is run. Localizing with a zone name
like "America/Los_Angeles" interprets each value using the
UTC offset in effect on that value’s own date, so values on either side
of a daylight saving switch are both stored correctly.
A naive datetime (one without tzinfo) is assumed to be
in the local timezone of the machine at the time of
upload, and that single UTC offset is applied to every value in
the upload.
naive_df <- data.frame(
sample_id = c("S3"),
collected_on = as.POSIXct(
c("2017-02-14 11:23:00")
)
)
table |> synStoreRows(values = naive_df)This is convenient when your data and your machine share a timezone
and daylight saving period, but any value whose date falls in a
different daylight saving period than the upload date is stored shifted
by one hour. Here, 2017-02-14 11:23 is a winter (PST,
UTC-8) wall-clock time; uploading it from a machine currently on summer
time (PDT, UTC-7) stores 18:23 UTC instead of the correct
19:23 UTC. Midnight values deserve extra care: the same
one-hour shift can move a naive midnight to 11 PM of the previous day,
changing the calendar date the value displays as. To avoid all of this,
localize your data first as shown in the previous example.
Plain R Date values (as opposed to datetime values like
POSIXct) are treated as midnight on that date
and converted the same way timezone-naive datetimes are: using the local
timezone of the machine at upload time, with the same
daylight-saving-shift risk described above.
date_df <- data.frame(
sample_id = "S4",
collected_on = as.Date("2017-02-14") # a date, not a datetime
)
table |> synStoreRows(values = date_df)
query_results <- synQuery(
sprintf("SELECT sample_id, collected_on FROM %s ORDER BY collected_on", table$id),
convert_to_datetime = TRUE
)
print(query_results)When storing rows from a CSV file, use date_columns and
date_format to tell the client which columns hold formatted
date strings and how to parse them. Including the UTC offset in the
strings (parsed by %z) keeps the values timezone-aware,
with the same benefits as localizing a DataFrame column.
By default, querying a table returns DATE columns as raw
millisecond timestamps rather than R date/time objects.
Don’t try to reconstruct a date from that raw value yourself. Synapse
stores DATE values as 64-bit epoch-millisecond integers,
but R has no native 64-bit integer type, and converting a raw integer
that large from Python to R isn’t a safe, bounds-checked cast the way
as.integer() is — the value can silently wrap around to a
different, still plausible-looking integer with no warning that anything
went wrong.
Instead, pass convert_to_datetime=TRUE so
DATE columns are converted to timezone-aware datetimes in
UTC on the Python side:
results <- synQuery(
sprintf("SELECT * FROM %s ORDER BY collected_on", table$id),
convert_to_datetime = TRUE
)
resultsThe collected_on column above may still print as a plain
number rather than a date — that’s misleading, not wrong. Each converted
value crosses from Python into R individually rather than as one
vectorized column, so collected_on comes back as an R
list-column: one correct POSIXct object
per cell rather than a single date/time vector. R’s default
data.frame printing silently drops the class when
displaying a list-column, showing the raw epoch-seconds number
underneath instead. Flatten it into a normal POSIXct vector
with c() before using it:
DATE columns are returned as UTC datetimes. Set the display timezone to view the same instants on another wall clock:
Note how the timezone-aware rows (S1, S2,
S5, S6) round-trip back to the exact
wall-clock times they were created with, while the naive datetime row
S3 and date row S4 were converted using that
same upload-time PDT offset. For S4, that offset is applied
to midnight because dates have no time of day.
To filter on a DATE column in a query, compare it
against epoch milliseconds:
# In a WHERE clause, compare a DATE column against epoch milliseconds:
# Example: filter for rows where 'collected_on' is on or after January 1, 2018
cutoff <- as.numeric(as.POSIXct("2018-01-01 00:00:00", tz = "UTC")) * 1000 # as double
recent <- synQuery(
sprintf("SELECT sample_id FROM %s WHERE collected_on >= %.0f", table$id, cutoff),
include_row_id_and_row_version = FALSE
)
recentFor the full details of how datetime values are interpreted during
upload, see the synStoreRows reference documentation.