To connect to data, set the Server property to the hostname or IP address of the Couchbase server(s) you are authenticating to. If your Couchbase server is configured to use SSL, you can enable it either by using an https URL for Server (like 'https://couchbase.server'), or by setting the UseSSL property to True.
Connecting to Couchbase Analytics Service
By default, the provider connects to the N1QL Query service. In order to connect to the Couchbase Analytics service, you will also need to set the CouchbaseService property to Analytics.
Authenticating with Standard Authentication
To authenticate with standard authentication, set the following:
- User: The user authenticating to Couchbase.
- Password: The password of the user authenticating to Couchbase.
Authenticating with Client Certificates
The provider supports authenticating with client certificates when SSL is enabled. To use client certificate authentication, set the following properties. Note that the User and Password options are not required when using client certificates.
- SSLClientCertType: Required. The type of client certificate set within SSLClientCert.
- SSLClientCert: Required. The client certificate in the format given by SSLClientCertType. Usually the path to your private key file.
- SSLClientCertPassword: Optional. The password of the client certificate if it is encrypted.
- SSLClientCertSubject: Optional. The subject of the client certificate, by default the first certificate found in the store. This is required if more than one certificate is available in the certificate store.
Authenticating Using a Credentials File
You can also authenticate using using a CredentialsFile. For more information on how to format your credentials file, refer to Couchbase's documentation. Note that the User and Password are not required when using a credentials file.
NoSQL Database
Couchbase is a schema-free document database that provides high performance, availability, and scalability. These features are not necessarily incompatible with a standards-compliant query language like SQL-92.
The provider models the schema-free Couchbase objects into relational tables and translates SQL queries into N1QL or SQL++ (Analytics) queries to get the requested data. In this section we will show various schemes that the provider offers to bridge the gap with relational SQL and a document database.
Automatic Schema Discovery
When the provider first connects to Couchbase, it opens each bucket and scans a configurable number of rows from that bucket. It uses those rows to determine the columns in that bucket and their data types, as well as how to build flavored and child tables for any arrays within those documents. For Couchbase Enterprise version 4.5.1 and later, the provider may can also be configured to use the INFER command when TypeDetectionScheme is set to INFER. This allows the provider to get a more accurate column listing for the bucket, and to detect more complex flavors.
When using the Analytics service, the provider only does column and child table detection. Flavored tables are provided by Couchbase itself using shadow datasets. Also, Analytics mode does not currently have INFER support, so only row scan is supported.
For more details, refer to Automatic Schema Discovery to see how flavored tables and child tables are modelled from Couchbase data.
Custom Schema Definitions
Optionally, you can use Custom Schema Definitions to project your chosen relational structure on top of a Couchbase object. This allows you to define your chosen column names, their data types, and the locations of their values in the Couchbase document.
Query Mapping
See Query Mapping for more details on how various N1QL and SQL++ operations are represented as SQL.
Vertical Flattening
See Vertical Flattening for more details on how arrays and objects are mapped into fields.
JSON Functions
See JSON Functions for more details on how to extract data from raw JSON strings.
Automatic Schema Discovery
Child Tables
If the documents within a bucket contain fields with arrays, then the provider will expose those fields as their own tables in addition to exposing them as JSON aggregates on the main table. The structure of these child tables depends upon whether the array contains objects or primitive values.
Array Child Tables
If the arrays contain primitive values like numbers or strings, the child table will have only two columns: one called "Document.Id" which is the primary key of the document containing the array, and one called "value" which contains the value within the array. For example, if the bucket "Games" contains these documents:
/* Primary key "1" */ { "scores" : [1,2,3] } /* Primary key "2" */ { "scores" : [4,5,6] } |
The provider will build a table called "Games_scores" containing these rows:
Document.Id | value |
1 | 1 |
1 | 2 |
1 | 3 |
2 | 4 |
2 | 5 |
2 | 6 |
Object Child Tables
If the arrays contain objects, the child table will have a column for each field that occurs within the objects, as well as a "Document.Id" column which contains the primary key of the document containing the array. For example, if the bucket "Games" contains these documents:
/* Primary key "1" */ { "moves" : [ { "piece" : "pawn" , "square" : "c3" }, { "piece" : "rook" , "square" : "d5" } ] } /* Primary key "2" */ { "moves" : [ { "piece" : "knight" , "square" : "f1" }, { "piece" : "bishop" , "square" : "e4" } ] } |
The provider will build a table called "Games_moves" containing these rows:
Document.Id | piece | square |
1 | pawn | c3 |
1 | rook | d5 |
2 | knight | f1 |
2 | biship | e4 |
Flavored Table
The provider can also detect when there are multiple types of documents within the same bucket, as long as TypeDetectionScheme is set to Infer or DocType and CouchbaseService is set to N1QL. These different types of documents are exposed as their own tables containing only the appropriate rows.
For example, the bucket "Games" contains documents which have a "type" value of either "chess" or "football":
/* Primary key "1" */ { "type" : "chess" , "result" : "stalemate" } /* Primary key "2" */ { "type" : "chess" , "result" : "black win" } /* Primary key "3" */ { "type" : "football" , "score" : 23 } /* Primary key "4" */ { "type" : "football" , "score" : 18 } |
The provider will create three tables for this bucket: one called "Games" which contains all the documents:
Document.Id | result | score | type |
1 | stalemate | NULL | chess |
2 | black win | NULL | chess |
3 | NULL | 23 | football |
4 | NULL | 18 | football |
One called "Games.chess" which contains only documents where the type is "chess":
Document.Id | result | type |
1 | stalemate | chess |
2 | black win | chess |
And one called "Games.football" which contains only documents where the type is "football":
Document.Id | score | type |
3 | 23 | football |
4 | 18 | football |
Note that the provider will not include columns in a flavored table that are not defined on the documents in that flavor. For example, even though both the "result" and "score" columns are included on the base table, "Games.chess" only includes "result" and "Games.football" only includes "score".
Flavored Child Tables
It is also possible for a flavored table to contain arrays, which will become their own child tables. For example, if the bucket "Games" contains these documents:
/* Primary key "1" */ { "type" : "chess" , "results" : [ "stalemate" , "white win" ] } /* Primary key "2" */ { "type" : "chess" , "results" : [ "black win" , "stalemate" ] } /* Primary key "3" */ { "type" : "football" , "scores" : [23, 12] } /* Primary key "4" */ { "type" : "football" , "scores" : [18, 36] } |
Table Name | Child Field | Flavor Condition |
Games | ||
Games_results | results | |
Games_scores | scores | |
Games.chess | "type" = "chess" | |
Games.chess_results | results | "type" = "chess" |
Games.football | "type" = "football" | |
Games.football_scores | scores | "type" = "football" |
Query Mapping
The provider maps SQL-92-compliant queries into corresponding N1QL or SQL++ queries. Although the mapping below is not complete, it should help you get a sense for the common patterns the provider uses during this transformation.
SELECT Queries
The SELECT statements are translated to the appropriate N1QL SELECT query as shown below. Due to the similarities between SQL-92 and N1QL, many queries will simply be direct translations.
One major difference is that when the schema for a given Couchbase bucket exists in the provider, a SELECT * query will be translated to directly select the individual fields in the bucket. The provider will also automatically create a "Document.Id" column based on the primary key of each document in the bucket.
SQL Query | N1QL Query |
SELECT * FROM users | SELECT META(`users`).id AS `id`, ... FROM `users` |
SELECT [Document.Id], status FROM users | SELECT META(`users`).id AS `Document.Id`, `users`.`status` FROM `users` |
SELECT * FROM users WHERE status = 'A' OR age = 50 | SELECT META(`users`).id AS `id`, ... FROM `users` WHERE TOSTRING(`users`.`status`) = "A" OR TONUMBER(`users`.`age`) = 50 |
SELECT * FROM users WHERE name LIKE 'A%' | SELECT META(`users`).id AS `id`, ... FROM `users` WHERE TOSTRING(`users`.`name`) LIKE "A%" |
SELECT * FROM users WHERE status = 'A' ORDER BY [Document.Id] DESC | SELECT META(`users`).id AS `id`, ... FROM `users` WHERE TOSTRING(`users`.`status`) = "A" ORDER BY META(`users`).id DESC |
SELECT * FROM users WHERE status IN ('A', 'B') | SELECT META(`users`).id, ... FROM `users` WHERE TOSTRING(`users`.`status`) IN ["A", "B"] |
Note that conditions can include extra type functions if the provider detects that a type conversion may be necessary. You can disable these type conversions using the StrictComparison property. For clarity, the rest of the N1QL samples are shown without these extra conversion functions.
Child Tables
As long as all the child tables in a query share the same parent, and they are combined using INNER JOINs on their "Document.Id" columns, the provider will combine the JOINs into a single UNNEST expression. Unlike N1QL UNNEST queries, you must explicitly JOIN with the base table if you want to access its fields.
SQL Query | N1QL Query |
SELECT * FROM users_posts | SELECT META(`users`).id, `users_posts`.`text`, ... FROM `users` UNNEST `users`.`posts` AS `users_posts` |
SELECT * FROM users INNER JOIN users_posts ON users.[Document.Id] = users_posts.[Document.Id] | SELECT META(`users`).id, `users`.`name`, ..., `users_posts`.`text`, ... FROM `users` UNNEST `users`.`posts` AS `users_posts` |
SELECT * FROM users INNER JOIN users_posts ... INNER JOIN users_comments ON ... | SELECT ... FROM `users` UNNEST `users`.`posts` AS `users_posts` UNNEST `users`.`comments` AS `users_comments` |
Flavor Tables
Flavored tables always have the appropriate condition included when you query, so that only documents from the flavor will be returned:
SQL Query | N1QL Query |
SELECT * FROM [users.subscriber] | SELECT ... FROM `users` WHERE `docType` = "subscriber" |
SELECT * FROM [users.subscriber] WHERE age > 50 | SELECT ... FROM `users` WHERE `docType` = "subscriber" AND `age` > 50 |
Aggregate Queries
N1QL has several built-in aggregate functions. The provider makes extensive use of this for various aggregate queries. See some examples below:
SQL Query | N1QL Query |
SELECT Count(*) As Count FROM Orders | SELECT Count(*) AS `count` FROM `Orders` |
SELECT Sum(price) As total FROM Orders | SELECT Sum(`price`) As `total` FROM `Orders` |
SELECT cust_id, Sum(price) As total FROM Orders GROUP BY cust_id ORDER BY total | SELECT `cust_id`, Sum(`price`) As `total` FROM `Orders` GROUP BY `cust_id` ORDER BY `total` |
SELECT cust_id, ord_date, Sum(price) As total FROM Orders GROUP BY cust_id, ord_date Having total > 250 | SELECT `cust_id`, `ord_date`, Sum(`price`) As `total` FROM `Orders` GROUP BY `cust_id`, `ord_date` Having `total` > 250 |
Insert Statements
The SQL INSERT statement is mapped to the N1QL INSERT statement as shown below. This works the same for both top-level fields as well as fields produced by Vertical Flattening:SQL Query | N1QL Query |
INSERT INTO users([Document.Id], age, status) VALUES ('bcd001', 45, 'A') | INSERT INTO `users`(KEY, VALUE) VALUES ('bcd001', { "age" : 45, "status" : "A" }) |
INSERT INTO users([Document.Id], [metrics.posts]) VALUES ('bcd002', 0) | INSERT INTO `users`(KEY, VALUE) VALUES ('bcd002', {"metrics': {"posts": 0}}) |
Child Table Inserts
Inserts on child tables are converted internally into N1QL UPDATEs using array operations. Since that this does not create the top-level document, the Document.Id provided must refer to a document that already exists.Another limitation of child table inserts is that multi-valued inserts must all use the same Document.Id. The provider will verify this before modifying any data and raise an error if this constraint is violated.
SQL Query | N1QL Query |
INSERT INTO users_ratings([Document.Id], value) VALUES ('bcd001', 4.8), ('bcd001', 3.2) | UPDATE `users` USE KEYS "bcd001" SET `ratings` = ARRAY_PUT(`ratings`, 4.8, 3.2) |
INSERT INTO users_reviews([Document.Id], score) VALUES ('bcd002', 'Great'), ('bcd002', 'Lacking') | UPDATE `users` USE KEYS "bcd001" SET `ratings` = ARRAY_PUT(`ratings`, {"score": "Great"}, {"score": "Lacking"}) |
Bulk Insert Statements
Bulk inserts are also supported the SQL Bulk Insert is converted as shown below:
INSERT INTO users#Temp([Document.Id], KEY, VALUE) VALUES( 'bcd001' , 45, "A" ) INSERT INTO users#Temp([Document.Id], KEY, VALUE) VALUES( 'bcd002' , 24, "B" ) INSERT INTO users SELECT * FROM users#Temp |
INSERT INTO `users` (KEY, VALUE) VALUES ( 'bcd001' , { "age" : 45, "status" : "A" }), ( 'bcd002' , { "age" : 24, "status" : "B" }) |
Like multi-valued inserts on child tables, all the rows in a bulk insert must also have the same Document.Id.
Update Statements
The SQL UPDATE statement is mapped to the N1SQL UPDATE statement as shown below:SQL Query | N1QL Query |
UPDATE users SET status = 'C' WHERE [Document.Id] = 'bcd001' | UPDATE `users` SET `status` = "C" WHERE META(`users`).id = "bcd001" |
UPDATE users SET status = 'C' WHERE age > 45 | UPDATE `users` SET `status` = "C" WHERE `age` > 45 |
Child Table Updates
When updating a child table, the SQL query is converted to an UPDATE query using either a "FOR" expression or an "ARRAY" expression:SQL Query | N1QL Query |
UPDATE users_ratings SET value = 5.0 WHERE value > 5.0 | UPDATE `users` SET `ratings` = ARRAY CASE WHEN `value` > 5.0 THEN 5 ELSE `value` END FOR `value` IN `ratings` END |
UPDATE users_reviews SET score = 'Unknown' WHERE score = '' | UPDATE `users` SET `$child`.`score` = 'Unknown' FOR `$child` IN `reviews` WHEN `$child`.`score` = "" END |
Flavor Table Updates
Like flavor table SELECTs, UPDATEs on flavor tables always include the appropriate condition, so only docments belonging to the flavor are affected:SQL Query | N1QL Query |
UPDATE [users.subscriber] SET status = 'C' WHERE age > 45 | UPDATE `users` SET `status` = "C" WHERE `docType` = "subscriber" AND `age` > 45 |
Delete Statements
The SQL DELETE statement is mapped to the N1QL DELETE statement as shown below:SQL Query | N1QL Query |
DELETE FROM users WHERE [Document.Id] = 'bcd001' | DELETE FROM `users` WHERE META(`users`).id = "bcd001" |
DELETE FROM users WHERE status = 'inactive' | DELETE FROM `users` WHERE `status` = "inactive" |
Child Table Deletes
When deleting from a child table, the SQL query is converted to an UPDATE query using an "ARRAY" expression:SQL Query | N1QL Query |
DELETE FROM users_ratings WHERE value < 0 | UPDATE `users` SET `ratings` = ARRAY `value` FOR `value` IN `ratings` WHEN NOT (`value` < 0) END |
DELETE FROM users_reviews WHERE score = '' | UPDATE `users` SET `reviews` = ARRAY `$child` FOR `$child` IN `reviews` WHEN NOT (`$child`.`score` = "") END |
Flavor Tables Deletes
Like flavor table SELECTs, DELETEs on flavor tables always include the appropriate condition, so only docments belonging to the flavor are affected:SQL Query | N1QL Query |
DELETE FROM [users.subscriber] WHERE status = 'inactive' | DELETE FROM `users` WHERE `docType` = "subscriber" AND status = "inactive" |
JSON Functions
The provider can return JSON structures as column values. The provider enables you to use standard SQL functions to work with these JSON structures. The examples in this section use the following array:
[ { "grade" : "A" , "score" : 2 }, { "grade" : "A" , "score" : 6 }, { "grade" : "A" , "score" : 10 }, { "grade" : "A" , "score" : 9 }, { "grade" : "B" , "score" : 14 } ] |
JSON_EXTRACT
The JSON_EXTRACT function can extract individual values from a JSON object. The following query returns the values shown below based on the JSON path passed as the second argument to the function:
SELECT Name, JSON_EXTRACT(grades, '[0].grade' ) AS Grade, JSON_EXTRACT(grades, '[0].score' ) AS Score FROM Students; |
Column Name | Example Value |
Grade | A |
Score | 2 |
JSON_COUNT
The JSON_COUNT function returns the number of elements in a JSON array within a JSON object. The following query returns the number of elements specified by the JSON path passed as the second argument to the function:
SELECT Name, JSON_COUNT(grades, '[x]' ) AS NumberOfGrades FROM Students; |
Column Name | Example Value |
NumberOfGrades | 5 |
JSON_SUM
The JSON_SUM function returns the sum of the numeric values of a JSON array within a JSON object. The following query returns the total of the values specified by the JSON path passed as the second argument to the function:
SELECT Name, JSON_SUM(score, '[x].score' ) AS TotalScore FROM Students; |
Column Name | Example Value |
TotalScore | 41 |
JSON_MIN
The JSON_MIN function returns the lowest numeric value of a JSON array within a JSON object. The following query returns the minimum value specified by the JSON path passed as the second argument to the function:
SELECT Name, JSON_MIN(score, '[x].score' ) AS LowestScore FROM Students; |
Column Name | Example Value |
LowestScore | 2 |
JSON_MAX
The JSON_MAX function returns the highest numeric value of a JSON array within a JSON object. The following query returns the maximum value specified by the JSON path passed as the second argument to the function:
SELECT Name, JSON_MAX(score, '[x].score' ) AS HighestScore FROM Students; |
Column Name | Example Value |
HighestScore | 14 |
DOCUMENT
The DOCUMENT function can be used to return an document as a JSON string. DOCUMENT(*) can be used with any type of SELECT query, including queries including other columns, queries including just DOCUMENT(*), and even more complex queries like JOINs.
SELECT [Document.Id], grade, score, DOCUMENT(*) FROM grades |
Document.Id | grade | score | DOCUMENT |
1 | A | 6 | {"document.id":1,"grade":"A","score":6} |
2 | A | 10 | {"document.id":1,"grade":"A","score":10} |
3 | A | 9 | {"document.id":1,"grade":"A","score":9} |
4 | B | 14 | {"document.id":1,"grade":"B","score":14} |
When used alone, DOCUMENT(*) returns the structure directly from Couchbase as if a N1QL or SQL++ SELECT * query were used. This means that no Document.Id value will be present since Couchbase does not include it automatically.
SELECT DOCUMENT(*) FROM grades |
DOCUMENT |
{"grades":{"grade":"A","score":6"}} |
{"grades":{"grade":"A","score":10"}} |
{"grades":{"grade":"A","score":9"}} |
{"grades":{"grade":"B","score":14"}} |
Custom Schema Definitions
In addition to Automatic Schema Discovery the provider also allows you to statically define the schema for your Couchbase object. Schemas are defined in text-based configuration files, which makes them easy to extend. You can call the CreateSchema stored procedure to generate a schema file; see Automatic Schema Discovery for more information.
The following sections show how to extend the resulting schema or write your own.
Example Document
Let's consider the document below and extract out the nested properties as their own columns:
/* Primary key "1" */ { "id" : 12, "name" : "Lohia Manufacturers Inc." , "homeaddress" : { "street" : "Main " Street ", " city ": " Chapel Hill ", " state ": " NC"}, "workaddress" : { "street" : "10th " Street ", " city ": " Chapel Hill ", " state ": " NC"} "offices" : [ "Chapel Hill" , "London" , "New York" ] "annual_revenue" : 35600000 } /* Primary key "2" */ { "id" : 15, "name" : "Piago Industries" , "homeaddress" : {street ": " Main Street ", " city ": " San Francisco ", " state ": " CA"}, "workaddress" : {street ": " 10th Street ", " city ": " San Francisco ", " state ": " CA"} "offices" : [ "Durham" , "San Francisco" ] "annual_revenue" : 42600000 } |
Custom Schema Definition
< rsb:info title = "Customers" description = "Customers" other:dataverse = "" other:bucket = customers "" other:flavorexpr = "" other:flavorvalue = "" other:isarray = "false" other:pathspec = "" other:childpath = "" > < attr name = "document.id" xs:type = "string" key = "true" other:iskey = "true" other:pathspec = "" /> < attr name = "annual_revenue" xs:type = "integer" other:iskey = "false" other:pathspec = "" other:field = "annual_revenue" /> < attr name = "homeaddress.city" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "homeaddress.city" /> < attr name = "homeaddress.state" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "homeaddress.state" /> < attr name = "homeaddress.street" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "homeaddress.street" /> < attr name = "name" xs:type = "string" other:iskey = "false" other:pathspec = "" other:field = "name" /> < attr name = "id" xs:type = "integer" other:iskey = "false" other:pathspec = "" other:field = "id" /> < attr name = "offices" xs:type = "string" other:iskey = "false" other:pathspec = "" other:field = "offices" /> < attr name = "offices.0" xs:type = "string" other:iskey = "false" other:pathspec = "[" other:field = "offices.0" /> < attr name = "offices.1" xs:type = "string" other:iskey = "false" other:pathspec = "[" other:field = "offices.1" /> < attr name = "workaddress.city" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "workaddress.city" /> < attr name = "workaddress.state" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "workaddress.state" /> < attr name = "workaddress.street" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "workaddress.street" /> </ rsb:info > |
Table Properties
The schema above uses the following properties to define specific qualities for the whole table. All of them are required:Property | Meaning |
other:dataverse | The name of the dataverse the dataset belongs to. Empty if not an Analytics view. |
other:bucket | The name of the bucket or dataset within Couchbase |
other:flavorexpr | The URL encoded condition in a flavored table. For example, "%60docType%60%20%3D%20%22chess%22". |
other:flavorvalue | The name of the flavor in a flavored table. For example, "chess". |
other:isarray | Whether the table is an array child table. |
other:pathspec | This is used to interpret the separators within other:childpath. See Column Properties for more details. |
other:childpath | The path to the attribute that is used to UNNEST the child table. Empty if not a child table. |
Column Properties
The schema above uses the following properties to define specific qualities for each column:Property | Meaning |
name | Required. The name of the column, lower-cased. |
key | Used to mark the primary key. Required for Document.Id but optional for other columns. |
xs:type | Required. The type of the column within the provider. |
other:iskey | Required. Must be the same value as key, or "false" if key is not included. |
other:pathspec | Required. This is used to interpret the separators within other:field. |
other:field | Required. The path to the field in Couchbase. |
Note that the fields which are produced by vertical flattening use the same syntax for separating array values and field values. This introduces a potential ambiguity in cases like the following, where the provider exposes the columns "numeric_object.0" and "array.0":
{ "numeric_object" : { "0" : 0 }, "array" : [ 0 ] } |
For example, with a field of "a.0.b.1" and a "pathspec" of "[{[", the N1QL expression "a[0].b[1]" would be generated. If instead the "pathspec" were "{{{", then the N1QL expression "a.`0`.b.`1`" would be generated.
Custom Schema Example
In this section is a complete schema. The info section enables a relational view of a Couchbase object. For more details, see Custom Schema Definitions. The table below allows the SELECT, INSERT, UPDATE, and DELETE commands as implemented in the GET, POST, MERGE, and DELETE sections of the schema below. The operations, such as couchbaseadoSysData, are internal implementations.
< rsb:script xmlns:rsb = "http://www.rssbus.com/ns/rsbscript/2" > < rsb:info title = "Customers" description = "Customers" other:dataverse = "" other:bucket = customers "" other:flavorexpr = "" other:flavorvalue = "" other:isarray = "false" other:pathspec = "" other:childpath = "" > < attr name = "document.id" xs:type = "string" key = "true" other:iskey = "true" other:pathspec = "" /> < attr name = "annual_revenue" xs:type = "integer" other:iskey = "false" other:pathspec = "" other:field = "annual_revenue" /> < attr name = "homeaddress.city" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "homeaddress.city" /> < attr name = "homeaddress.state" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "homeaddress.state" /> < attr name = "homeaddress.street" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "homeaddress.street" /> < attr name = "name" xs:type = "string" other:iskey = "false" other:pathspec = "" other:field = "name" /> < attr name = "id" xs:type = "integer" other:iskey = "false" other:pathspec = "" other:field = "id" /> < attr name = "offices" xs:type = "string" other:iskey = "false" other:pathspec = "" other:field = "offices" /> < attr name = "offices.0" xs:type = "string" other:iskey = "false" other:pathspec = "[" other:field = "offices.0" /> < attr name = "offices.1" xs:type = "string" other:iskey = "false" other:pathspec = "[" other:field = "offices.1" /> < attr name = "workaddress.city" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "workaddress.city" /> < attr name = "workaddress.state" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "workaddress.state" /> < attr name = "workaddress.street" xs:type = "string" other:iskey = "false" other:pathspec = "{" other:field = "workaddress.street" /> </ rsb:info > </ rsb:script > |