Oracle Database Integration Guide
This guide covers how to connect an Oracle Database to BurnLedger, configure query templates, set up a read-only database user, and choose the right hash scope for your compliance needs.
BurnLedger uses the pure-Go go-ora driver, so no Oracle Instant Client, TNS admin directory, or tnsnames.ora entry is involved — the whole connection is expressed as a single URL-style DSN.
Connection Configuration
The Oracle connector takes exactly one configuration field, dsn. No other key is
read. The connection config is decoded as a flat string→string object, so extra
string keys are ignored, but a non-string value anywhere in the object (for
example "read_only": true, which this connector does not use) fails decoding
with invalid config JSON when BurnLedger connects.
| Key | Type | Required | Description |
|---|---|---|---|
dsn |
string | Yes | Oracle connection URL: oracle://<user>:<password>@<host>:<port>/<service_name>?SSL=true |
Format:
oracle://<user>:<password>@<host>:<port>/<service_name>?SSL=true
Percent-encode special characters in the password. The DSN is parsed as a URI both by BurnLedger's host validation and by the driver, so characters such as
@ : / ? #must be escaped (@→%40,:→%3A,/→%2F,?→%3F,#→%23). An unescaped@splits the URI at the wrong place, and BurnLedger then validates — or rejects — the wrong host. If you control the credential, the simplest option is a password limited to letters, digits,-and_.
Example:
oracle://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@oracle.example.com:1521/FREEPDB1?SSL=true
REPLACE-WITH-YOUR-PASSWORD is a placeholder, not a suggestion: choose your own password when you create the user below, and substitute it wherever the placeholder appears in this guide.
Service name, not SID
The path segment after the host and port is a service name, not a SID. On a
container database this is normally the pluggable database's service — FREEPDB1
on Oracle Free 23c, ORCLPDB1 on many 19c installs — not the CDB instance name
(FREE, ORCL). Confirm it with:
SELECT name FROM v$services;
-- or, from the database host:
-- lsnrctl services
Pointing the DSN at a SID where the listener expects a service name produces
ORA-12514 ("listener does not currently know of service requested"), surfaced by
BurnLedger as CONNECTION_FAILED.
Apart from one addition described below, BurnLedger passes the DSN to the driver unchanged, so driver-level query parameters you append are preserved. Only the service-name form has been verified end to end against a live database.
The CID parameter
BurnLedger appends a CID query parameter to the DSN if you have not set one:
CID=(CID=(PROGRAM=burnledger)(HOST=burnledger-enclave)(USER=burnledger))
This is required because the attestation enclave has no kernel hostname; without an
explicit CID the driver emits (HOST=(none)), which Oracle's TNS descriptor parser
rejects with TNS-01153 / ORA-12564. If you supply your own CID, it is used
verbatim — make sure it is a well-formed, parenthesis-balanced TNS client
identifier. In Oracle audit trails, sessions from BurnLedger appear under program
name burnledger.
When registering the system via the SDK:
TypeScript
const system = await bl.registerSystem({
name: "users-db",
connectorType: "oracle",
connectionConfig: {
dsn: "oracle://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@oracle.example.com:1521/FREEPDB1?SSL=true",
},
subjectQuery: "SELECT ID, EMAIL, PHONE FROM APPOWNER.USERS WHERE EMAIL = :1",
});
Python
system = bl.register_system(
name="users-db",
connector_type="oracle",
connection_config={
"dsn": "oracle://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@oracle.example.com:1521/FREEPDB1?SSL=true",
},
subject_query="SELECT ID, EMAIL, PHONE FROM APPOWNER.USERS WHERE EMAIL = :1",
)
Connection requirements:
| Requirement | Details |
|---|---|
| Transport security | ?SSL=true is required, and the endpoint must be a TCPS listener with a verification record that chains to a trusted root. SSL=true is what sets the protocol to tcps — the URL scheme is ignored by the driver, so tcps:// or oracles:// on their own change nothing. Leave SSL VERIFY at its default (true); SSL VERIFY=false encrypts without validating the verification record and is refused, as is a plain oracle:// DSN with no SSL=true. Oracle Native Network Encryption (ENCRYPTION=REQUIRED) does not satisfy this: the server picks the algorithm and no peer is authenticated. A DSN that sets both connStr and server is refused as unclassifiable, because the driver's endpoint set is then order-dependent. For a private or internal CA, supply the root as ca_cert — a wallet is a directory inside the process that dials, which the hosted deployment cannot receive. See connector transport security. |
| Network access | BurnLedger connects from a fixed egress address. Allowlist it on the listener port in your firewall or security group — that is the TCPS listener's port (commonly 2484, or 1521 if your listener serves TCPS there), not a cleartext TCP one. |
| Host policy | The DSN host is resolved and checked before connecting; private, loopback, and link-local addresses are refused in production deployments. |
| Permissions | Read-only. BurnLedger introspects the credential's privileges at connect time and refuses to connect if any of them can write. See Read-Only User Setup below. |
| Transaction mode | Every attestation query runs on a pinned connection after SET TRANSACTION READ ONLY, so any write a template tried to perform fails at the engine level. |
| Timeout | Queries must complete within the system's query_timeout (default 30 seconds). |
Private CA (ca_cert)
A TCPS listener whose verification record is signed by an internal CA — the usual
arrangement for on-prem Oracle — cannot be verified against the system trust
store, and the handshake fails with x509: certificate signed by unknown
authority. Supply the root in the connection config as ca_cert: base64 of the
PEM bundle, snake_case in both SDKs.
base64 -w0 ca.pem # macOS: base64 -i ca.pem | tr -d '\n'
{
"dsn": "oracle://burnledger_ro:pw@oracle.example.com:2484/FREEPDB1?SSL=true",
"ca_cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t..."
}
ca_cert does not change the transport classification: a custom trust root
is still verification, so the config stays verified and is admitted by the
ADR-012 floor. It also never turns TLS on — supplying it alongside a DSN
that puts no TLS on the data path is refused when the connector is built, rather
than connecting with a trust root that does nothing.
Two Oracle-specific restrictions come from the driver, and both are refusals rather than surprises:
- Exactly one server address. go-ora rewrites the TLS server name on the
shared configuration at every dial, so with several addresses (a second
?server=, or a descriptor listing anADDRESS_LIST) verification would depend on which dial won the race. Aca_certon such a DSN is refused. SSL VERIFYmust stay at its defaulttrue. Handing the driver a trust root bypasses its own verify switch, so a DSN asking both for a root and for no verification cannot be honoured as written and is refused.
Query Template Format
A query template is a SELECT statement with :1 as the placeholder for the
data subject identifier. Oracle binds positional parameters as :1, :2, …, and
BurnLedger supplies exactly one bind value — the subject identifier — using a
parameterized query (never string interpolation).
:1is the only placeholder that works here. There is no universal placeholder across BurnLedger connectors: PostgreSQL uses$1, MySQL uses?, Oracle uses:1. A template with no recognized placeholder is rejected at registration withINVALID_QUERY_TEMPLATE(HTTP 422). This rejection is deliberate: a template that never binds the subject matches nothing, and a silently empty result would produce a signed verification record asserting that the subject has no records — a cryptographically valid false negative.The registration validator also tolerates a literal
?for historical reasons, but the Oracle driver does not bind?. Such a template registers and then fails at attestation time with an Oracle syntax/bind error. Always write:1.
Rules:
- Must be a single statement beginning with
SELECT(orWITH … SELECT). - Must contain a
WHEREclause — an unbounded full-table scan is rejected. - Must contain the
:1placeholder. Exactly one bind value — the subject identifier — is supplied, so:1is the only bind the template may reference; a:2(or a named bind such as:email) has no value and fails at attestation time withORA-01008. - Must not contain an embedded
;— a trailing semicolon is not needed and breaks the inline view BurnLedger wraps the template in (ORA-00933). Leave it off. - Must not contain write keywords (
INSERT,UPDATE,DELETE,MERGE,TRUNCATE,CREATE,DROP,ALTER, …), including inside aWITHclause. - Must not use
FOR UPDATE— row locks are a write effect.
Examples:
-- Single table, subject identified by email
SELECT ID, EMAIL, NAME, CREATED_AT FROM APPOWNER.USERS WHERE EMAIL = :1
-- Subject identified by an external ID
SELECT ID, NAME, EMAIL, PHONE FROM APPOWNER.CUSTOMERS WHERE EXTERNAL_ID = :1
-- Join across tables for a complete subject record
SELECT u.ID, u.EMAIL, p.STREET, p.CITY, p.COUNTRY
FROM APPOWNER.USERS u
LEFT JOIN APPOWNER.PROFILES p ON p.USER_ID = u.ID
WHERE u.EMAIL = :1
-- Case-insensitive match on a normalized column
SELECT ID, EMAIL FROM APPOWNER.USERS WHERE LOWER(EMAIL) = LOWER(:1)
Schema qualification. The read-only user owns no objects, so unqualified names
resolve against its own (empty) schema and fail with ORA-00942. Qualify every
table with its owner (APPOWNER.USERS) or create private synonyms for the
read-only user.
Column naming and stability. Every returned column is read as a nullable
string and hashed together with its column name, and Oracle reports unquoted
column names in upper case. Fields are sorted by column name before the row is
hashed, so reordering the select list does not change a record hash — but
renaming or aliasing a column, adding or removing one, or changing a value does.
List columns explicitly (a SELECT * is accepted but produces a warning, and its
column set can drift with the table) and alias expressions deterministically:
SELECT ID, TO_CHAR(CREATED_AT, 'YYYY-MM-DD"T"HH24:MI:SS') AS CREATED_AT
FROM APPOWNER.USERS WHERE EMAIL = :1
Cast binary and LOB columns explicitly (RAWTOHEX(...), TO_CHAR(...)) rather
than selecting them raw — the connector reads results as text.
Important: Design your query to return all records associated with the data subject that you need to prove exist (or have been deleted). If a subject's data spans multiple schemas or databases, use joins or register multiple systems.
Read-Only User Setup
BurnLedger requires a dedicated read-only Oracle user. It does not take your
word for it and there is no read_only configuration flag for this connector:
at connect time it introspects the credential's effective privileges and aborts
with WRITE_ACCESS_DETECTED if it finds any that can write. Nothing is ever
written to your database — the check is pure introspection of two data-dictionary
views that any session can read:
| View | What BurnLedger accepts | Anything else means write-capable |
|---|---|---|
SESSION_PRIVS (system privileges active in the session, including those from default roles) |
CREATE SESSION, SELECT ANY TABLE, SELECT ANY DICTIONARY, READ ANY TABLE |
CREATE TABLE, CREATE ANY TABLE, INSERT/UPDATE/DELETE ANY TABLE, DROP ANY …, ALTER ANY …, UNLIMITED TABLESPACE, … |
USER_TAB_PRIVS WHERE GRANTEE = USER (object privileges granted directly to the user) |
SELECT, READ |
INSERT, UPDATE, DELETE, ALTER, INDEX, REFERENCES, EXECUTE, … |
Two consequences worth planning for:
- Do not grant
RESOURCE,DBA,CONNECT, orUNLIMITED TABLESPACE. Those bring system privileges intoSESSION_PRIVSand the connection is refused.CONNECTis not a safe shortcut: on 12c and later it also carriesSET CONTAINER, which is outside the accepted set, so a user grantedCONNECTis rejected withWRITE_ACCESS_DETECTED. GrantCREATE SESSIONdirectly — it is the only system privilege the connector needs. - Grant
SELECTdirectly to the user, not through a role. The object-privilege check readsUSER_TAB_PRIVSfor grants made directly to the user; privileges reaching the user only via a role are not visible there, so a role-based grant may pass the check and then fail the query withORA-00942.
Create the user
Run as a DBA inside the pluggable database that holds your data. In a CDB,
connect to the PDB first — creating a local user from the root fails with
ORA-65096 unless the name starts with C##:
ALTER SESSION SET CONTAINER = FREEPDB1;
-- Create the account. Quote the password if it contains anything but
-- letters, digits and underscore.
CREATE USER burnledger_ro IDENTIFIED BY "REPLACE-WITH-YOUR-PASSWORD";
-- The only system privilege it needs: the ability to log in.
GRANT CREATE SESSION TO burnledger_ro;
-- Read access to exactly the tables your query template touches.
GRANT SELECT ON APPOWNER.USERS TO burnledger_ro;
GRANT SELECT ON APPOWNER.PROFILES TO burnledger_ro;
Do not add a tablespace quota, RESOURCE, or any ANY-scoped write privilege.
SELECT ANY TABLE / READ ANY TABLE are accepted if your policy prefers them
over per-table grants, but per-table grants are the tighter option.
Verify the permissions
Connect as burnledger_ro and run exactly what BurnLedger runs:
-- Expect: only CREATE SESSION (plus, optionally, SELECT ANY TABLE /
-- SELECT ANY DICTIONARY / READ ANY TABLE). Anything else is rejected.
SELECT PRIVILEGE FROM SESSION_PRIVS;
-- Expect: only SELECT or READ rows.
SELECT PRIVILEGE, OWNER, TABLE_NAME FROM USER_TAB_PRIVS WHERE GRANTEE = USER;
-- The query itself must succeed...
SELECT COUNT(*) FROM APPOWNER.USERS WHERE ROWNUM <= 1;
-- ...and writes must fail with ORA-01031 / ORA-00942.
INSERT INTO APPOWNER.USERS (ID) VALUES (-1);
UPDATE APPOWNER.USERS SET EMAIL = 'x' WHERE ID = -1;
DELETE FROM APPOWNER.USERS WHERE ID = -1;
Revoke write privileges if they exist
REVOKE RESOURCE FROM burnledger_ro;
REVOKE UNLIMITED TABLESPACE FROM burnledger_ro;
REVOKE INSERT, UPDATE, DELETE ON APPOWNER.USERS FROM burnledger_ro;
ALTER USER burnledger_ro QUOTA 0 ON USERS;
Re-run the two verification queries above; when they return only the accepted privileges, BurnLedger will connect.
Hash Scope Options
The hashScope parameter is recorded on the system and carried into every
verification record, so a verifier can see what the hashes cover.
full (recommended for most cases)
Every column returned by the template is canonicalized with its column name (a
NULL is encoded with a distinct null sentinel, so NULL and an empty string
hash differently) and the row is hashed with SHA-256. Any change to any selected
column value produces a different hash.
const system = await bl.registerSystem({
// ...
});
Use when: You need to prove the exact data that existed, or prove that data was deleted and not merely modified (e.g., nullifying PII columns is not the same as deletion under some interpretations of GDPR).
existence (default when omitted)
Declares that the attestation is about whether records exist and how many, not about their content.
const system = await bl.registerSystem({
// ...
hashScope: "existence",
});
Use when: You only need to prove that records existed and were later removed.
How it interacts with proof mode
For Oracle, what BurnLedger actually executes is chosen by the attestation's proof mode, not by the hash scope:
| Proof mode | What runs | What lands in the verification record |
|---|---|---|
count |
SELECT COUNT(*) over your template, bounded by ROWNUM |
Record count only — no row content leaves the database |
merkle |
Your template, bounded by ROWNUM, one SHA-256 per row |
Record count + Merkle root over the per-row hashes |
Row hashing under merkle always covers the full canonicalized row for SQL
connectors. If you do not want row content to influence any hash, use proof mode
count.
Comparison
full |
existence |
|
|---|---|---|
| Detects row deletion | Yes | Yes |
| Detects row insertion | Yes | Yes |
| Detects column value changes | Yes (merkle root only; see the note below the table) |
Not asserted |
| Hash includes row content | Yes (with merkle proofs) |
No, when used with count |
| Recommended for GDPR Art. 17 | Yes | Acceptable |
"Detects" here is bounded by what a merkle certificate actually carries: a
signed Merkle root over the row hashes and nothing else — BurnLedger serves no
per-record inclusion proof, so a changed value is demonstrable only to someone
who already holds every matching row and rebuilds the tree, and from format 7.0
the leaves are keyed inside the enclave so no party, BurnLedger included, can
open the root at all (see
Certificate Scope §4).
End-to-End Example
This example walks through the full process: creating a read-only user, registering the system, creating an attestation before deletion, performing the deletion, verifying, and generating a verification record.
1. Database setup (run once)
-- As a DBA, inside the PDB that holds the data
ALTER SESSION SET CONTAINER = FREEPDB1;
CREATE USER burnledger_ro IDENTIFIED BY "REPLACE-WITH-YOUR-PASSWORD";
GRANT CREATE SESSION TO burnledger_ro;
GRANT SELECT ON APPOWNER.USERS TO burnledger_ro;
GRANT SELECT ON APPOWNER.PROFILES TO burnledger_ro;
GRANT SELECT ON APPOWNER.ORDERS TO burnledger_ro;
2. Application code
TypeScript
import { BurnLedger } from "burnledger";
import fs from "node:fs/promises";
const bl = new BurnLedger({
apiKey: process.env.BURNLEDGER_API_KEY,
});
// Register the system (run once, store the system ID)
const system = await bl.registerSystem({
name: "myapp-users-oracle",
connectorType: "oracle",
connectionConfig: {
dsn: "oracle://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@oracle.example.com:1521/FREEPDB1?SSL=true",
},
subjectQuery:
"SELECT u.ID, u.EMAIL, u.NAME, p.PHONE, p.ADDRESS FROM APPOWNER.USERS u LEFT JOIN APPOWNER.PROFILES p ON p.USER_ID = u.ID WHERE u.EMAIL = :1",
});
// --- When a deletion request comes in ---
const subjectEmail = "jane.doe@example.com";
// Step 1: Attest that the data currently exists
const attestation = await bl.attest(subjectEmail, {
systemIds: [system.id],
proofMode: "merkle",
});
for (const s of attestation.systems) {
console.log(`${s.systemName}: ${s.recordCount} records found`);
}
// Step 2: Delete the data in your application
// await conn.execute("DELETE FROM APPOWNER.PROFILES WHERE USER_ID = (SELECT ID FROM APPOWNER.USERS WHERE EMAIL = :1)", [subjectEmail]);
// await conn.execute("DELETE FROM APPOWNER.USERS WHERE EMAIL = :1", [subjectEmail]);
// await conn.commit();
// Step 3: Verify the deletion
// The certificate is ISSUED BY verify() once the data is gone -- there is
// no separate "create certificate" step.
const result = await bl.verify(attestation.id, subjectEmail, { timeout: 60 });
for (const s of result.systems) {
console.log(`${s.systemName}: now ${s.recordCount} records`);
}
if (result.certificate) {
await bl.savePdf(result.certificate.id, `erasure-cert-${subjectEmail}.pdf`);
console.log(`Certificate issued: ${result.certificate.id}`);
console.log(`Transparency status: ${result.certificate.transparencyStatus}`);
} else {
console.log(`Not certified: status ${result.status}`);
}
Python
import os
from burnledger import BurnLedger
bl = BurnLedger(api_key=os.environ["BURNLEDGER_API_KEY"])
# Register the system (run once, store the system ID)
system = bl.register_system(
name="myapp-users-oracle",
connector_type="oracle",
connection_config={
"dsn": "oracle://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@oracle.example.com:1521/FREEPDB1?SSL=true",
},
subject_query=(
"SELECT u.ID, u.EMAIL, u.NAME, p.PHONE, p.ADDRESS "
"FROM APPOWNER.USERS u LEFT JOIN APPOWNER.PROFILES p ON p.USER_ID = u.ID "
"WHERE u.EMAIL = :1"
),
)
# --- When a deletion request comes in ---
subject_email = "jane.doe@example.com"
# Step 1: Attest that the data currently exists
attestation = bl.attest(
subject_email,
system_ids=[system.id],
proof_mode="merkle",
)
for s in attestation.systems:
print(f"{s.system_name}: {s.record_count} records found")
# Step 2: Delete the data in your application
# cursor.execute("DELETE FROM APPOWNER.PROFILES WHERE USER_ID = (SELECT ID FROM APPOWNER.USERS WHERE EMAIL = :1)", [subject_email])
# cursor.execute("DELETE FROM APPOWNER.USERS WHERE EMAIL = :1", [subject_email])
# connection.commit()
# Step 3: Verify the deletion
# The certificate is ISSUED BY verify() once the data is gone -- there is
# no separate "create certificate" step.
result = bl.verify(attestation.id, subject_email, timeout=60)
for s in result.systems:
print(f"{s.system_name}: now {s.record_count} records")
if result.certificate:
bl.save_pdf(result.certificate.id, f"erasure-cert-{subject_email}.pdf")
print(f"Certificate issued: {result.certificate.id}")
print(f"Transparency status: {result.certificate.transparency_status.value}")
else:
print(f"Not certified: status {result.status.value}")
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
CONNECTION_FAILED with missing required config field "dsn" (or invalid config JSON) |
Connection config used another key name, or a non-string value | The Oracle connector reads only dsn, as a string. Note that registration itself is not blocked by this — the system is stored with a failed health status and every attestation against it fails until the config is corrected. |
CONNECTION_FAILED with ORA-12514 |
The DSN path is a SID, or the service name is wrong | Use the service name (e.g. FREEPDB1); confirm with SELECT name FROM v$services or lsnrctl services. |
CONNECTION_FAILED with ORA-01017 |
Wrong user/password — often an unescaped special character in the password | Percent-encode @ : / ? # in the DSN password. |
CONNECTION_FAILED / blocked or invalid host |
The DSN could not be parsed, or resolved to a private/loopback address | Check the URI syntax (again, password encoding) and that the host is publicly resolvable; allowlist the BurnLedger IPs on port 1521. |
CONNECTION_FAILED with ORA-12564 or TNS-01153 |
A custom CID parameter in your DSN is malformed |
Remove it and let BurnLedger supply its own, or provide a balanced (CID=(PROGRAM=…)(HOST=…)(USER=…)). |
CONNECTION_FAILED on attestation: "connector oracle: transport security plaintext is below the required minimum verified". A health check reports only connection failed — read transport_security on the system |
The DSN has no SSL=true, so the driver uses the tcp protocol regardless of the URL scheme |
Add ?SSL=true and point the DSN at a TCPS listener. |
CONNECTION_FAILED on attestation: "connector oracle: transport security encrypted is below the required minimum verified". A health check reports only connection failed — read transport_security on the system |
SSL VERIFY=false — TLS with no certificate validation |
Remove it (the driver default is true) and use a listener certificate that chains to a trusted root. |
CONNECTION_FAILED with "failed to verify credential privileges" |
The user cannot read SESSION_PRIVS or USER_TAB_PRIVS |
Both are normally readable by any session; check for a logon trigger, restricted session, or a hardened profile blocking dictionary access. |
WRITE_ACCESS_DETECTED |
The user holds a system privilege outside CREATE SESSION / SELECT ANY TABLE / SELECT ANY DICTIONARY / READ ANY TABLE, or an object privilege other than SELECT / READ |
Revoke it (commonly CONNECT — which carries SET CONTAINER on 12c+ — RESOURCE, UNLIMITED TABLESPACE, or a stray INSERT). Registration is rejected outright in this case, not merely marked unhealthy. See Read-Only User Setup. |
INVALID_QUERY_TEMPLATE (422) with "does not contain :1" |
The template has no subject placeholder | Bind the subject with :1 — not $1, ?, :name, or {identifier}. |
INVALID_QUERY_TEMPLATE (422) with "no WHERE clause" |
The template would scan the whole table | Add a WHERE <column> = :1 predicate. |
INVALID_QUERY_TEMPLATE (422) with "single statement" / "disallowed keyword" |
Embedded ;, or a write keyword anywhere in the template |
Submit one SELECT (or WITH … SELECT), no trailing semicolon, no DML. |
ORA-00933 at attestation time |
Trailing semicolon in the template — it is wrapped in an inline view | Remove the semicolon. |
ORA-00942 at attestation time |
Table not qualified with its owner, or SELECT was granted via a role |
Qualify as OWNER.TABLE and grant SELECT directly to the read-only user. |
ORA-01008 at attestation time |
The template references a bind other than :1 (e.g. :2, or a named bind like :email) |
Bind only :1; only the subject identifier is supplied. |
RECORD_LIMIT_EXCEEDED |
The query matched more rows than max_records (default 1,000,000) |
Narrow the template, or raise max_records up to the plan limit. |
CONNECTION_FAILED with "query timed out" |
The query exceeded query_timeout (default 30s) |
Index the predicate column, or raise query_timeout at registration. |
QUERY_HASH_MISMATCH during verification |
The stored template changed, or data changed between attestation and verification | Expected if you deleted or modified records — review the changes array. If you edited the template, register a new system. |