Requirements for an OData service (reference)
The driver talks to any OData v4 service that offers the required features below. If a service also
offers one of the extensions, the driver uses it; without it, the driver behaves as if it didn't
exist. You need this page if you check whether a service works with the driver, or if you build a
service for Access applications yourself. The data service Wkl.Data.Gateway offers all features on
this page.
Syntax
GET <service-root>/$metadata schema document (CSDL, XML) GET <service-root>/<entity-set>?$select=…&$filter=… reading, JSON response POST/PATCH/DELETE <service-root>/<entity-set>[(<key>)] writing
Parameters
The features are grouped by how binding they are for the driver: without the requirements for
reading, the driver doesn't connect or reads incorrectly; without the requirements for writing,
all sets remain read-only; the extensions are recommendations whose absence turns off a feature
but falsifies nothing. The fundamentals of OData v4 are described in the OASIS standards:
OData Version 4.01 Part 1: Protocol,
Part 2: URL Conventions
(query options, literals), OData Common Schema Definition Language (CSDL) XML Representation 4.01
($metadata), and OData JSON Format 4.01.
The driver requires only features that OData 4.0 already has; it uses $query (4.01) if available.
Required for reading
| Feature | What the driver needs it for |
|---|---|
$metadata (CSDL) with entity sets, properties, types, facets, and keys | catalog functions, type mapping (see Escape clauses and data types) |
Only the entity sets released for the caller in $metadata | Access offers what the user may also read. That's a requirement for the service's security, not for the technology: the driver connects without it too, but then shows tables that fail to read |
$select, $filter, $orderby, $top, $skip | translation of SELECT |
In $filter, the OData v4 literals without type names, also for Edm.Guid (Id eq 3333…), and comparisons of two properties (ParentId eq Id) | conditions on uniqueidentifier columns; roots of a tree. A service without field-to-field comparison answers 400, the driver reports 42000 – only this one condition fails |
$count | COUNT(*) without loading the rows |
@odata.nextLink | reading large sets page by page |
Prefer: odata.maxpagesize (recommended) | larger pages when only narrow columns are read, such as the keys. A service that ignores the preference returns its own page size; the driver then reads more pages, and nothing else changes |
| Responses as JSON | the driver doesn't read Atom/XML |
Required for writing
| Feature | What the driver needs it for |
|---|---|
Key in the entity type and addressing Set(Key) | PATCH, DELETE (reading uses $filter, never the key address); a set without a key is read-only (except with the actions for tables without a key, below) |
POST, PATCH, DELETE with a JSON body; response with the stored record | adding, changing, deleting |
If-Match, response 412 for an outdated version, announcement with Core.OptimisticConcurrency | detecting write conflicts; without this announcement, the driver rejects changing and deleting (adding remains possible). The driver builds the ETag itself from the announced column (binary value as W/"<Base64>"); the service must accept this form, see What the driver assumes when writing |
Extensions (recommended) that the driver uses if the service offers them
| Feature | How the driver recognizes it | What for |
|---|---|---|
Address for custom statements: accepts {"sql": …, "parameters": {…}}, answers {"columns": […], "data": […], "affectedRowCount": …} | the connection names it (PASSTHROUGH), or $metadata names it in Store.PassThrough | statements that can't be translated, pass-through queries |
Org.OData.Aggregation.V1.ApplySupported with the transformations filter, compute, groupby, aggregate; Aggregation.CustomAggregationMethods with Wkl.Store.V1.count | announcement in $metadata | totals, groups, HAVING, DISTINCT without custom statements; COUNT(Spalte) |
Bound actions UpdateByOriginal and DeleteByOriginal on a set without a key | $metadata declares both for the type of the set | changing and deleting by the old values |
POST <resource>/$query with the query options in the body (OData 4.01) | tried as soon as an address would be longer than MAXURLLENGTH; without this form, the service answers 404, 405, or 501, and the request goes as GET after all | long conditions |
| Sign-in through HTTP Negotiate | the connection turns it on (AUTH=WINDOWS); the service requests it with 401 and WWW-Authenticate: Negotiate | Windows sign-in without a secret |
| Sign-in through HTTP Basic | the connection names UID and PWD | sign-in with name and password |
| Key in a header | APIKEY/APIKEYENV or AUTH=KEY; header APIKEYHEADER, default X-Api-Key | service keys and personal keys |
Second factor: 401 with WWW-Authenticate: Wkl-Totp … endpoint="<path>"; POST <path> with {"code": "…"} returns 201 with a session | challenge in the response | code prompt, session in the X-Wkl-Session header |
Identity choice: header X-Wkl-Role; rejection 403 with X-Wkl-Roles (percent-encoded UTF-8, separated by commas) | header in the response | choice of the database identity |
Idempotency-Key on POST, PATCH, DELETE: a retry receives the stored first response | – (the driver always sends the key) | retry after a dropped connection without a duplicate record |
Vocabulary Wkl.Store.V1 (terms below) | annotations in $metadata | appearing as a database product, schema as TABLE_SCHEM, type names, indexes, statistics for Access's query planning |
Transactions: Store.Transactions on the entity container names an address; POST to it returns 201 with {"id": "…"}; requests with the header Wkl-Transaction: <id> run in it; POST <address>/<id>/commit or /rollback closes it (204); an unknown or ended ID 404, one rolled back by the service 409 | annotation in $metadata | BeginTrans/CommitTrans/Rollback, BeginTransaction; SQL_TXN_CAPABLE = SQL_TC_ALL |
Database messages in error.details of a rejection: per entry code = message number as text, message, plus sqlState (five characters) | entries with a number as code | one diagnostic record per message with the number as the native error number and the stated SQLSTATE (if missing: 42000), as with the SQL Server driver; without such entries, one record with the HTTP status as the number |
Core.Computed | annotation on the property | columns that can't be written (identity, computed, row version); the driver reports them as read-only through SQLColAttribute (SQL_DESC_UPDATABLE), all others as “unknown” |
Terms of the Wkl.Store.V1 vocabulary. The vocabulary is an extension of this product, not part
of OData. A service that offers it places it under its own address and includes it with
edmx:Reference (example below); the driver recognizes the terms by their namespace, with or without
an alias.
| Term | Type | Applies to | Meaning for the driver |
|---|---|---|---|
Product | Edm.String | entity container | name of the database product, reported through SQLGetInfo(SQL_DBMS_NAME); without the term, the driver reports OData and version 4.0 |
ProductVersion | Edm.String | entity container | major and minor version (17.00), reported as SQL_DBMS_VER in the form 17.00.0000 |
Schema | Edm.String | entity container | schema of the service root, reported as TABLE_SCHEM; without it, Access stores an empty source name for a table linked as dbo.Tabelle |
PassThrough | Edm.String | entity container | address for custom statements; adopted only at the same origin as the service root |
Transactions | Edm.String | entity container | address that opens a transaction |
Type | Edm.String | property | database type name, for a self-numbering column with identity |
Indexes | list of Index (Name, Unique, Primary, Clustered, Columns) | entity type | indexes for SQLStatistics |
Rows, Pages | Edm.Int64 | entity type | rows and data pages for SQLStatistics (CARDINALITY, PAGES); estimates |
Commitments per kind of service
Which of the driver's commitments applies depends on what the service offers and honors. The driver
recognizes announcements in $metadata, but not whether a service actually honors a header such as
Idempotency-Key. What it can't recognize, you must clarify with the service's provider or verify
yourself before use.
| Commitment | any OData v4 service with the required features | service with the extensions on this page | data service Wkl.Data.Gateway |
|---|---|---|---|
| Reading, filtering, sorting, counting on the service | yes | yes | yes |
| Writing with write conflict detection | yes, with Core.OptimisticConcurrency | yes | yes |
| Retry of a read after a dropped connection | yes | yes | yes |
| Retry of a change without a duplicate record | no: the driver retries by default anyway; a service that ignores Idempotency-Key can create a record twice. Remedy: RETRY=OFF | only if the service honors the header – not detectable, clarify with the provider | yes: the service keeps the response in memory for ten minutes |
| Transactions | no (HYC00; no effect in DAO) | with Store.Transactions | yes |
| Totals and groups without pass-through | no | with Aggregation.ApplySupported | yes |
| Statements that can't be translated | no, 42000 | with a pass-through address | yes: checked statements for every caller whose rule names the alias; unchecked SQL only when enabled |
| Second factor, identity choice | no | with the challenges on this page | yes |
What a service must commit to for retries. The driver retries a change at most twice, after 1
and after 3 seconds, with the same Idempotency-Key. The service must therefore keep the first
response for a key for at least a few seconds and return exactly that response to the retry, without
executing the change again. The driver doesn't retry an exceeded time limit (HYT00): whether the
change was executed is then unknown, and the application must read the record again. It doesn't
retry the completion of a transaction (commit, rollback) either.
What a service must commit to for security reasons. It refuses a request without a valid sign-in or second factor before it executes anything; the driver then sends it again. And it doesn't redirect addresses under the service root to another origin (see Residual risks).
Return value
None. The driver checks the features when connecting and at the time of each use, not in advance.
Remarks
- Without
Core.OptimisticConcurrency, Access puts the old values of all columns into the condition when changing. That can't be expressed in OData; the driver rejects it instead of dropping the check. The remedy lies with the service, for example with a computed concurrency property. - Without grouping and without an address for custom statements, totals queries and joins from Access fail with “ODBC–call failed”; see Troubleshooting.
- Without
Idempotency-Keysupport, a retry after a dropped connection can create a record twice or report a write conflict even though the change went through. A service that doesn't know the header ignores it; the driver doesn't detect that. The header is described by an expired IETF draft, not a standard (see Standards and specifications). - Custom statements on the wire. The driver sends
{"sql": …, "parameters": {…}}and expects{"columns": […], "data": […], "affectedRowCount": …}. Bound values remain values: the?placeholders, which ODBC counts by position, become named placeholders – a question mark in a string, a bracketed name, or a comment remains untouched. A response withoutcolumnsis rejected. Inferring the types from the values of the first row would mean: an integer in a decimal column becomes an integer, a timestamp becomes text, and an empty result has no shape at all – while the application binds its buffers before it reads the first row. - Output parameters, further result sets, describing. When an application binds output parameters, the driver also sends
"outputs": {"p1": "Edm.Int32", "p2": {"type": "Edm.String", "maxLength": 20}}and reads the values fromoutputsin the response; it reads further result sets fromresults(each withcolumns,data,truncated). When an application asks for the columns before execution, the driver sends"describe": trueand expects the columns of the first set without the service running anything. It asks for a description only from a service that names the address inStore.PassThroughitself: a service that ignoresdescribewould otherwise run the statement. If a service returns nooutputs, the application getsNULLas the output value. - Description of the columns. Besides
name,type, and the facets (maxLength,precision,scale), the driver reads three optional members per column:storeType(the database's type name, likeStore.Typein$metadata),identity(truefor a value the database assigns), andlabel(the name the database gives the column, where it differs fromname). Rows are JSON objects, so for a column without a name or with a name already taken, the service has to assign a key of its own. Withlabel, the driver shows the application the database's name – empty or repeated, as ODBC Driver 18 does – and reads the values undername. Withoutlabel, the application sees that key. Store.PassThroughapplies only at the same origin (scheme, host, port) as the service root.- The driver skips fields of a response that it doesn't know. On the pass-through route, it reads
columns,data,affectedRowCount,truncated,results, andoutputs(see Limits). - No redirect to another origin. The driver follows a redirect only when reading and only to the same scheme, host, and port; if the service redirects elsewhere, the request fails with
08S01.
Example
Excerpt of a schema document with the extensions that Access needs for editing and grouping:
<edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml"> <edmx:Include Namespace="Org.OData.Aggregation.V1" Alias="Aggregation" /> </edmx:Reference> <edmx:Reference Uri="https://daten.example.com/odata/v4/$vocabulary/Wkl.Store.V1.xml"> <edmx:Include Namespace="Wkl.Store.V1" Alias="Store" /> </edmx:Reference> … <EntityContainer Name="Container"> <Annotation Term="Store.Product" String="Microsoft SQL Server" /> <Annotation Term="Store.ProductVersion" String="17.00" /> <Annotation Term="Store.Schema" String="dbo" /> <Annotation Term="Aggregation.ApplySupported"> <Record> <PropertyValue Property="Transformations"> <Collection><String>filter</String><String>compute</String><String>groupby</String><String>aggregate</String></Collection> </PropertyValue> <PropertyValue Property="Rollup" EnumMember="Aggregation.RollupType/None" /> </Record> </Annotation> <Annotation Term="Aggregation.CustomAggregationMethods"> <Collection><String>Wkl.Store.V1.count</String></Collection> </Annotation> </EntityContainer>
This is what the data service returns (abridged). The terms appear with the alias that the
edmx:Reference assigns; the driver reads both notations.
Applies to
Driver Wkl OData, both bitnesses. OData v2 and v3, as well as services without $metadata or
without JSON, aren't supported.
Exceptions
| SQLSTATE | Condition |
|---|---|
42000 | A statement can't be translated, and there's no address for custom statements |
HYC00 | The application requests a transaction, and the service doesn't offer any (no Store.Transactions) |
40001 | SQLEndTran with commit: the service has already rolled back the transaction (after an error that made it unusable, or after its idle time); none of it is saved |
08S01 | The service isn't reachable, or a document breaks off (for example a streamed response) |