Transactional DDL and Known Limitations¶
Added in version 0.8.0.
CREATE, DROP, and ALTER SEMANTIC VIEW are fully transactional: they participate in your surrounding BEGIN / COMMIT / ROLLBACK block the way ordinary DuckDB DDL does. ADBC, dbt-duckdb, and any other transaction-aware client see the same semantics.
This page covers the everyday behavior first, then a short list of edge cases. Most of them surface only in unusual situations – several connections issuing DDL against the same database at the same time, or scripts that change the parser setting the extension relies on.
For the common workload – open a database, run DDL at start-up, then query – the next two sections cover everything that applies.
DDL Now Participates in Your Transaction¶
You can wrap DDL in BEGIN / COMMIT and rely on the rollback semantics:
BEGIN;
CREATE SEMANTIC VIEW order_metrics AS
TABLES (o AS orders) DIMENSIONS (o.region AS o.region);
ROLLBACK;
-- order_metrics does not exist; the CREATE was discarded.
BEGIN;
DROP SEMANTIC VIEW order_metrics;
ROLLBACK;
-- order_metrics is still there; the DROP was discarded.
BEGIN;
ALTER SEMANTIC VIEW order_metrics RENAME TO sales_metrics;
ROLLBACK;
-- the view is still called order_metrics.
This applies to every CREATE body variant: the AS keyword body, inline FROM YAML $$ ... $$, FROM YAML FILE '<path>', and the CREATE OR REPLACE / IF NOT EXISTS modifiers. ALTER covers RENAME TO, SET COMMENT, and UNSET COMMENT.
Reads Inside an Open Transaction See Your Changes¶
Changed in version 0.16.0: Earlier releases read every semantic view’s definition as last committed, so a view created, dropped or renamed in an open transaction did not show up that way until you committed.
Everything that reads a semantic view runs in your transaction: SEMANTIC_VIEW(…) and semantic_view(), explain_semantic_view(), DESCRIBE SEMANTIC VIEW, SHOW SEMANTIC VIEWS and the other SHOW SEMANTIC * commands, READ_YAML_FROM_SEMANTIC_VIEW and GET_DDL. They see your own uncommitted changes, as a query of an ordinary table does:
BEGIN;
CREATE SEMANTIC VIEW v AS TABLES (o AS orders) DIMENSIONS (o.r AS o.region);
SHOW SEMANTIC VIEWS; -- v is listed
SELECT * FROM SEMANTIC_VIEW(v DIMENSIONS r); -- and can be queried
ROLLBACK;
SHOW SEMANTIC VIEWS; -- v is gone
An in-flight DROP hides the view, and an in-flight ALTER ... RENAME TO shows it under its new name, in your transaction only. Other connections see none of it until you commit.
The member types that SHOW and DESCRIBE report, and the plan explain_semantic_view() shows, come from your transaction too. A source table you alter in an open transaction, or a temporary table of your session, is typed and planned the way semantic_view() reads it.
CREATE IF NOT EXISTS Across Multiple Connections¶
Note
This section is mostly theoretical for typical DuckDB usage. DuckDB runs as an in-process library, and only one process at a time can open a database file for writing, so most deployments run their DDL from a single connection at start-up. In that setting CREATE SEMANTIC VIEW IF NOT EXISTS behaves as expected every time, and this section can be skipped.
If two connections to the same database (for example, two worker threads in one program, each with its own connection) both run CREATE SEMANTIC VIEW IF NOT EXISTS my_view ... at the same time, and neither has committed yet when the other checks, both will try to create the view. One will win. The other will see one of these errors, depending on whether the collision is caught as the row is written or when the transaction commits:
Constraint Error: Duplicate key "schema_name: main, name: my_view" violates primary key constraint.
TransactionContext Error: Failed to commit: PRIMARY KEY or UNIQUE constraint violation: duplicate key "main, my_view"
This is the same error a plain CREATE SEMANTIC VIEW would produce in the same race. IF NOT EXISTS reliably absorbs duplicates on a single connection; it cannot absorb two connections that both found no existing view at check time.
If you do run parallel set-up on several connections – worker threads that each bootstrap at start-up, or parallel set-up in a test harness – catch either error on your view name and treat it as success:
try:
conn.execute("CREATE SEMANTIC VIEW IF NOT EXISTS my_view ...")
except (duckdb.ConstraintException, duckdb.TransactionException) as e:
if "my_view" not in str(e):
raise
# Another connection created the view first; treat that as success.
The first writer wins, the second writer sees a clear error rather than silent corruption, and after the catch both connections see the same view.
DROP and ALTER Without IF EXISTS: the Existence Check and Its Race Window¶
A non-IF EXISTS DROP SEMANTIC VIEW my_view (or any non-IF EXISTS ALTER SEMANTIC VIEW my_view ... form) is rewritten into a small existence-check statement followed by the actual DELETE / UPDATE. If the view is not there when the check runs, you get:
Invalid Input Error: semantic view 'my_view' does not exist
rather than a silent success: the statement named a specific view, and that view was not there. The IF EXISTS variants (DROP SEMANTIC VIEW IF EXISTS my_view, ALTER SEMANTIC VIEW IF EXISTS my_view ...) keep their silent-no-op contract by design.
When a single connection issues all the DDL there is no race. The rest of this section matters only when several connections issue DDL against the same database at the same time.
The check and the write are separate statements, and whether they are atomic depends on your transaction:
Under autocommit (the default), they are not atomic. DuckDB commits after each statement, so the existence check and the
DELETE/UPDATErun in two separate transactions. A concurrent drop is caught only if it has already committed by the time your check runs. If another connection drops the view in the small window between your check and your write, the check passes and then:a plain
DROPdeletes 0 rows and reports success having removed nothing (a silent no-op);a plain
ALTER RENAMEwhose target name was taken in that window surfaces a rawConstraint Error: Duplicate keyfrom DuckDB rather than the extension’salready existsmessage.
Inside an explicit transaction, they are atomic. Wrap the DDL in
BEGIN ... COMMIT(or use a connection withautocommit = false) and the check and the write share one snapshot. A conflicting concurrent commit then makes yourCOMMITfail with a transaction-conflict error you can retry, instead of slipping through the window.
So if you run parallel DDL and need the check to be reliable, wrap it in a transaction:
BEGIN;
DROP SEMANTIC VIEW my_view;
COMMIT;
This is the DROP / ALTER counterpart of the CREATE IF NOT EXISTS race above; both come down to the same rule – concurrent DDL against one database needs an explicit transaction. Narrowing that window without one is a known limitation of the two-statement check-then-write sequence.
Read-Only Databases¶
Added in version 0.9.0.
Loading the extension into a read-only DuckDB database works the same way as a writable one – LOAD semantic_views succeeds and you can query any semantic view that was previously defined. The extension detects access_mode = 'read_only' at load time and skips the catalog-table bootstrap that would otherwise fail with DuckDB’s read-only error.
Three behaviors change between writable and read-only databases:
Reads work as usual on a bootstrapped database. If the database already contains a
semantic_layer._definitionstable (because it was opened writable before and one or more semantic views were defined), thenSHOW SEMANTIC VIEWS,DESCRIBE SEMANTIC VIEW name,FROM semantic_view('name', dimensions := [...], metrics := [...]), and the rest of the SHOW / DESCRIBE / GET_DDL family all behave identically to writable mode.A fresh read-only database is treated as having zero views, not as an error. If the database was never bootstrapped (no
semantic_layer._definitionstable exists),SHOW SEMANTIC VIEWSreturns zero rows. Each lookup returns its usual “no such view” error rather than a raw catalog error about a missing table:DESCRIBE SEMANTIC VIEW anythingreportssemantic view 'anything' does not exist, andFROM semantic_view('anything', ...)reportsSemantic view 'anything' not found. Run SHOW SEMANTIC VIEWS to see all registered views.DDL fails with DuckDB’s standard read-only error.
CREATE,DROP, andALTER SEMANTIC VIEWwrite tosemantic_layer._definitionson the caller’s connection, as anINSERT,DELETEorUPDATE. On a read-only database those writes fail with:Invalid Input Error: Cannot execute statement of type "INSERT" on database "<name>" which is attached in read-only mode!The exact statement-type token (
INSERT/DELETE/UPDATE) varies by DDL form. The extension does not wrap or rephrase the message.On a read-only database that was never bootstrapped,
CREATE SEMANTIC VIEWfails one step earlier, because the table it writes to does not exist:Catalog Error: Table with name "semantic_layer._definitions" does not exist because schema "semantic_layer" does not exist.Either way, define the views on a writable connection first, as shown below.
Bootstrap-then-Reopen Workflow¶
The typical pattern for shipping a read-only database with pre-defined semantic views is:
import duckdb
# Step 1 -- open writable, define views, close.
rw = duckdb.connect("analytics.duckdb")
rw.execute("LOAD semantic_views")
rw.execute("""
CREATE SEMANTIC VIEW orders AS
TABLES (o AS orders_table PRIMARY KEY (id))
DIMENSIONS (o.region AS o.region)
METRICS (o.total AS SUM(o.amount))
""")
rw.close()
# Step 2 -- reopen read-only and query.
ro = duckdb.connect("analytics.duckdb", read_only=True)
ro.execute("LOAD semantic_views")
rows = ro.execute(
"SELECT * FROM SEMANTIC_VIEW(orders DIMENSIONS region METRICS total)"
).fetchall()
Attached Databases (Single-Catalog)¶
Semantic-view definitions live in one place: a semantic_layer._definitions table created in the database you loaded the extension into (the primary database). The extension is single-catalog – it manages semantic views in that one database, not across ATTACH-ed databases.
Concretely, if you ATTACH another database and USE it, then run semantic-view DDL:
ATTACH 'other.db' AS other;
USE other;
CREATE SEMANTIC VIEW v AS ...; -- error, see below
you get an actionable error rather than a confusing failure or a silently-lost view:
semantic_views: semantic-view DDL was issued against database 'other', but the
semantic view catalog lives in a different database. Semantic views are
single-catalog: manage them from the database the extension was loaded into,
without USE-ing into an attached database.
Reads (SHOW SEMANTIC VIEWS, SEMANTIC_VIEW(...), DESCRIBE) always resolve against the primary catalog regardless of USE, so the rule is: run semantic-view DDL from the database you loaded the extension into. You can still reference tables from attached databases inside a view body by qualifying them (TABLES (o AS other.main.orders ...)) – only the _definitions catalog is single-database.
None of this arises if you do not ATTACH a second database, or if the session stays on the primary catalog.
Note
Because the _definitions table is tied to the primary database, the one-time
v0.1.0 companion-file migration only ever imports the primary database’s own
<db>.semantic_views companion file. An attached database’s companion file is
never read or removed.
Several Statements in One Call¶
Changed in version 0.16.0: Semantic-view DDL can share a query string with other statements. Earlier
releases refused such a string with semantic-view DDL must be sent as its
own statement, one statement per call.
A query string can hold semantic-view statements alongside other SQL, separated by semicolons. DuckDB splits the string and runs each statement in order, so one Python execute() call can set up a table and a view on it:
con.execute("""
CREATE TABLE orders (id INTEGER, region VARCHAR, amount DOUBLE);
INSERT INTO orders VALUES (1, 'US', 100), (2, 'EU', 200);
CREATE SEMANTIC VIEW sales AS
TABLES (o AS orders PRIMARY KEY (id))
DIMENSIONS (o.region AS o.region)
METRICS (o.revenue AS SUM(o.amount));
""")
DuckDB 2.0 runs a string’s statements one at a time and parses each one just before it runs. So when one statement fails, whether on a syntax error or while it runs, the statements before it have already run and their effects stay, and nothing after it runs. The exception is a string that opens its own transaction with BEGIN: the error aborts that transaction, so the statements before it are rolled back with it, and the connection needs a ROLLBACK before it runs anything else.
A view named with a DuckDB reserved word, such as select, has to be quoted in every statement, as DuckDB requires for its own names: CREATE SEMANTIC VIEW "select" AS .... Unquoted, the statement fails with a syntax error.
How the Extension Joins DuckDB’s Parser¶
Changed in version 0.16.0: The semantic-view statements are part of DuckDB’s own grammar on every
connection. Earlier releases also parsed a statement sent on its own
through DuckDB’s parser-override hook, which the
allow_parser_override_extension setting could switch off. That setting
no longer affects semantic-view statements.
CREATE SEMANTIC VIEW and the other semantic-view statements are not part of DuckDB’s built-in grammar. When the extension loads, it adds them, and the SEMANTIC_VIEW(…) query construct, to the PEG grammar DuckDB 2.0 parses every statement with, so DuckDB finds them wherever its own statements of the same kind may appear, and reports a malformed one as a syntax error with a LINE 1: ... ^ pointer, as it does for its own SQL.
Two things follow from DuckDB reading the statement as tokens:
CREATE SEMANTIC VIEWstores each expression as DuckDB’s tokens spell it: a tab becomes a space, a run of blank lines one line break, and comments are dropped.GET_DDLandDESCRIBEshow that spelling. The view behaves the same.A view name that is a reserved word must be quoted, as above.
DuckDB keeps the grammar a connection parses with per connection. The connection that runs LOAD semantic_views, and every connection opened after it, can use the statements at once. A connection that was already open when another connection loaded the extension picks them up only when it next starts or finishes a query. A statement that fails to parse never starts one, so until that connection runs a statement DuckDB parses on its own, such as SELECT 1, every semantic-view statement on it fails as a syntax error, and retrying it fails the same way. To avoid that, run LOAD semantic_views on the connection first (it is a no-op on a database that has the extension loaded), or open the connection after the LOAD.
other = con.cursor() # opened before the LOAD
con.execute("LOAD semantic_views")
other.execute("LOAD semantic_views") # activates the statements on this connection
other.execute("SHOW SEMANTIC VIEWS")
Summary¶
For most users, the everyday-visible behavior is:
BEGIN ... ROLLBACKrolls backCREATE,DROP, andALTER SEMANTIC VIEW.Queries,
SHOWandDESCRIBEinside an open transaction see its uncommitted semantic-view changes.
The other items on this page only matter in specific situations:
Concurrent
CREATE IF NOT EXISTSfrom two connections can produce a constraint or commit-conflict error on one of them; catch it and treat as success.Semantic views are single-catalog: run their DDL from the database you loaded the extension into, not from a
USE-d attached database.A connection that was already open when another one ran
LOAD semantic_viewsneeds one statement DuckDB parses on its own, such as its ownLOAD semantic_views, before it parses semantic-view statements. Retrying a failed semantic-view statement does not help.
See also:
Read-Only Databases – read-only database support and the bootstrap-then-reopen workflow.
Attached Databases (Single-Catalog) – single-catalog behavior with
ATTACH/USE.CREATE SEMANTIC VIEW – syntax for all four
CREATEbody forms.DROP SEMANTIC VIEW –
DROPandDROP IF EXISTS.ALTER SEMANTIC VIEW –
ALTERvariants.Error Messages – error catalog.