mysql
Source: builtin/mysql
MySQL and MariaDB data operations.
Provider configuration
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
dsn | string | yes | — | Connection string, either as a URL (mysql://user:pass@host:3306/db) or in the Go driver’s format (user:pass@tcp(host:3306)/db). Query parameters are passed to the driver as options. parseTime and multiStatements are always on. |
Actions
mysql_delete
Delete the rows matching where, checked against the table’s schema before the
run starts.
Remove a merchant’s test products.
action "mysql_delete" "test_products" {
table = "products"
where = { merchant_id = action.mysql_insert.merchant.id, name = "Test" }
}Attributes
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
table | string | yes | — | Table name in the connected database. |
where | any | yes | — | Object of column = value conditions, joined with AND; null matches NULL. Must name at least one column. |
Outputs
Type: object({rows_affected=number}).
rows_affected: how many rows were deleted.
mysql_insert
Insert one row. Values are sent as bind parameters, never pasted into SQL, and are checked against the table’s schema before the run starts: the table and columns exist, every value fits its column, every required column (not nullable, no default, not generated) is set, and no generated column is. The outputs are the inserted row, including generated ids and defaults.
orchard teardown deletes the row again by its primary key, unless the action
has its own lifecycle { teardown { } } block or sets
lifecycle { managed_teardown = false }.
Insert a merchant, then a product that references it.
action "mysql_insert" "merchant" {
table = "merchants"
values = { name = "Acme", region = "us" }
}
action "mysql_insert" "product" {
table = "products"
values = {
merchant_id = action.mysql_insert.merchant.id
name = "Widget"
price = 9.99
}
}Attributes
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
conflict_columns | list(string) | no | — | Columns that identify an existing row for on_conflict: the primary key (the default) or a unique key. values must set them. |
on_conflict | string | no | "error" | What to do when a row with the same conflict columns already exists: "error" (the database rejects the insert), "ignore" (leave the row as it is), or "update" (update it with values). With "ignore" or "update" the outputs are the existing row, and orchard teardown doesn’t delete it; an update of it is restored like a typed update. |
table | string | yes | — | Table name in the connected database. |
values | any | yes | — | Object of column = value. |
Outputs
Type: any.
The inserted row, keyed by column name. Columns come back typed by their kind: numbers, bools, strings, decoded JSON.
mysql_query
Execute a SQL statement. If it returns rows (a SELECT), the first row is
exposed as outputs keyed by column name. If it returns no rows, the outputs are
last_insert_id and rows_affected.
Insert a row, then use its AUTO_INCREMENT id in the next statement.
action "mysql_query" "create_merchant" {
query = "INSERT INTO merchants (name, region) VALUES ('Acme', 'us')"
}
action "mysql_query" "create_product" {
query = "INSERT INTO products (merchant_id, name) VALUES (${action.mysql_query.create_merchant.last_insert_id}, 'Widget')"
}Read a row back with SELECT; its columns become outputs.
action "mysql_query" "merchant" {
query = "SELECT id, name FROM merchants WHERE id = ${action.mysql_query.create_merchant.last_insert_id}"
}Bind values to ? placeholders with params instead of interpolating them into the SQL.
action "mysql_query" "merchant" {
query = "INSERT INTO merchants (name, region) VALUES (?, ?)"
params = [var.merchant_name, "eu"]
}Attributes
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
params | any | no | — | Values for the query’s ? placeholders, in order. They’re sent separately from the SQL, so they need no quoting or escaping, and null binds as NULL. A list or object goes as JSON text. A query with params is prepared on the server and must be a single statement. |
query | string | yes | — | The SQL statement to execute. |
Outputs
Type: any.
For a statement that returns rows: the first row of the result set, keyed by column name, or empty if no rows matched.
For a statement that returns no rows (INSERT, UPDATE, DELETE, or DDL):
| Output | Description |
|---|---|
last_insert_id | LAST_INSERT_ID(): the first AUTO_INCREMENT id the most recent INSERT generated on the connection. 0 if there hasn’t been one. |
rows_affected | ROW_COUNT(): rows the statement changed. An UPDATE that sets a column to its current value doesn’t count. |
Column values are mapped to cty as follows:
| MySQL type | cty type |
|---|---|
TINYINT…BIGINT (including BOOL), DECIMAL, FLOAT, DOUBLE | number |
CHAR, VARCHAR, TEXT, ENUM, JSON, BLOB, TIME | string |
DATE, DATETIME, TIMESTAMP | string (RFC 3339) |
NULL | null |
mysql_update
Update the rows matching where, with the same checks and bind parameters as
mysql_insert. Generated columns can’t be set.
orchard teardown writes back the values the update overwrote, for rows
inserted earlier in the same run. Set lifecycle { managed_teardown = true }
to restore other rows too, or false to restore nothing. Updates matching more
than 1,000 rows aren’t restored.
Suspend the merchant inserted earlier.
action "mysql_update" "suspend" {
table = "merchants"
set = { status = "suspended" }
where = { id = action.mysql_insert.merchant.id }
}Attributes
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
set | any | yes | — | Object of column = new value. |
table | string | yes | — | Table name in the connected database. |
where | any | yes | — | Object of column = value conditions, joined with AND; null matches NULL. Must name at least one column, so an update can’t hit every row by accident. Use mysql_query for anything more complex. |
Outputs
Type: object({rows_affected=number}).
rows_affected: how many rows the update changed.
Examples
A complete scenario, with teardown, reading the DSN from the environment.
scenario "mysql_demo" {
required_providers {
mysql = { source = "builtin/mysql" }
}
variable "dsn" {
default = env("DATABASE_URL")
}
provider "mysql" {
dsn = var.dsn
}
action "mysql_query" "create_merchant" {
query = "INSERT INTO merchants (name, region) VALUES ('Acme', 'us')"
lifecycle {
teardown {
query = "DELETE FROM merchants WHERE id = ${self.last_insert_id}"
}
}
}
output "merchant_id" { value = action.mysql_query.create_merchant.last_insert_id }
}Notes
- Orchard does not manage schema. Tables must exist before the scenario runs.
mysql_insert,mysql_updateandmysql_deleteare typed alternatives tomysql_query: values are bound as parameters, and each action is checked against the live schema before the run starts. A table that doesn’t exist yet (because an earlier action creates it) is checked when the action runs instead.- MySQL has no
RETURNINGclause. After anINSERT, wire the new row’s id into later steps withlast_insert_id(see the examples below). MariaDB 10.5+ does supportRETURNING, and its rows come back as outputs like anySELECT. - Each
mysql_queryruns on its own, outside any transaction spanning actions. Onequerycan hold several statements, so wrap them inSTART TRANSACTION; ... COMMIT;if they must apply atomically. With several statements,last_insert_idandrows_affecteddescribe the last one. - SQL injection: values interpolated via
${var.name}are HCL-evaluated to strings and substituted as text. Use scenario variables for known-safe values; never pass untrusted input through thequeryattribute. - Connection opening is lazy — a well-formed DSN configures the provider successfully; the first action call is where connection errors surface.