oRm 0.8.0
Breaking Changes
-
TableModel$read()no longer caps results at 100 rows..limitdefaulted to100, so any read wider than a hundred rows was silently truncated – the result looked complete and carried no warning. Every caller inherited the cap, includingall(),one_or_none(),get(),Record$relationship(), andTableModel$relationship(), so a one-to-many traversal over a large parent quietly lost rows, and code that counted or aggregated the result was wrong rather than merely short..limitnow defaults toNULL, meaning “return every matching row”. Explicit limits are unchanged: a positive.limitstill returns the first N rows, a negative one the last N, and.offsetstill paginates. Code that relied on the implicit cap must now pass.limit = 100itself..mode = "tbl"previously worked around the cap by resetting.limittoNULLwhen the argument was missing; that special case is gone, so all modes now share one default.
New Features
-
.autoflushreturns server-generated values for inserts made inside a transaction. Inside a transactionRecord$create()defaults to a plain insert, so IDENTITY/SERIAL keys and column defaults stayNULLon the record – the surprise reported in #122, where a parent key was needed for a child insert. Two new controls change that default, and the innermost one wins:-
with.Engine(..., .autoflush = TRUE)makes everycreate()in that block flush. -
Engine$new(..., .autoflush = TRUE)does the same for every transaction on the engine, so call sites need no flag at all;engine$set_autoflush()changes it later in the session.
An explicit
create(flush_record = ...)still overrides both. A block’s setting is scoped to that block and restored on exit (including on error), and a nestedwith.Engine()inherits it unless it says otherwise. Both default toFALSE, so existing behaviour is unchanged and bulk loads are not charged a round trip per row.The block-level argument is spelled
.autoflush, matchingEngine$new()and the rest of the package’s dot-prefixed arguments. Flushing behaviour is covered per dialect – PostgreSQL recovers keys withRETURNING, SQL Server withOUTPUT INSERTED, SQLite with a last-rowid lookup – intest-Dialect-postgres.R,test-Dialect-mssql-integration.R, andtest-Dialect-sqlite.R. -
Bug Fixes
-
SQL Server: records on trigger-bearing tables now recover their IDENTITY key.
OUTPUT INSERTEDis rejected on tables with triggers (error 334), soflush.mssqlfell back to a keyed re-read that only worked when the caller supplied every primary key value; an IDENTITY key raised an error instead. The fallback now recovers the IDENTITY key withSCOPE_IDENTITY()in the same batch as the insert and returns the full row, soRecord$create()behaves the same on trigger tables as on trigger-free ones. Keys that are neither supplied nor IDENTITY (aDEFAULT NEWID()GUID, say) still fail with a clear message.
Documentation
with.Engine()now documents its flush behaviour. Inside a transaction,Record$create()leaves server-generated values (IDENTITY/SERIAL keys, column defaults) unset unless it is asked to flush. The help page now says so, spells out all three ways to get them back (flush_record = TRUEon one insert,autoflush = TRUEon the block,.autoflush = TRUEon the engine), and shows the parent/child insert pattern;create()’s own docs cross-reference it (#122).The Engine vignette gains a “Server-generated values inside a transaction” section, showing with live output how
create()returns generated keys outside a transaction but not inside one, and working through all three ways to change that (per insert, per block, per engine), plus the bulk-load opt-out and a demonstration that a flushed insert still rolls back with its transaction. The Records vignette’sflush_recordparagraph is corrected – it described the in-transaction default as waiting for commit – and now links to that section.
oRm 0.7.0
New Features
-
Microsoft SQL Server dialect. oRm now speaks T-SQL, with the same reflection depth PostgreSQL has enjoyed. Connect through
odbcand the dialect is inferred from theDriver(or.connection_string) argument; where that is not enough — a DSN, for instance — pass.dialect = "mssql". SQL Server 2016 or later is assumed.-
Reflection reads the
syscatalog views, rebuilding declared types (nvarchar(100),decimal(10,2),nvarchar(MAX)), primary keys (composite included), nullability, column defaults, and foreign keys. Foreign keys becomeForeignKeyobjects, soengine$reflect_schema()wires upmany_to_onerelationships and their backrefs exactly as it does on PostgreSQL. -
Inserts use
OUTPUT INSERTED.*to return the stored row, so server-generated values land back on the record. Tables carrying triggers reject that clause; oRm falls back to a keyed re-read when the primary key was supplied, and otherwise reports why rather than surfacing a raw SQL Server error. -
CREATE TABLEis guarded withIF OBJECT_ID(...) IS NULL, since T-SQL has noCREATE TABLE IF NOT EXISTS. - Read-only engines fall back to oRm’s application-level statement guard, as SQL Server has no session-level read-only mode.
-
Reflection reads the
IDENTITY columns. Declared in the type string, mirroring how PostgreSQL’s
SERIALhas always worked:Column("INT IDENTITY(1,1)", primary_key = TRUE). IDENTITY columns are exempt from the required-field check oncreate()and are omitted from insert statements, so the server generates the value..dialectargument toEngine$new()overrides automatic detection. Any dialect string is accepted, so dialects shipped by other packages can be selected the same way.
Bug Fixes
-
Composite primary keys now generate valid DDL on every dialect. Marking more than one column
primary_key = TRUEemitted an inlinePRIMARY KEYclause per column, which every database rejects (SQLite, for example, withtable ... has more than one primary key).create_table()now emits a single table-levelPRIMARY KEY (a, b)constraint and marks those columnsNOT NULL. Single-column keys render exactly as before. The query side —update(),delete(),refresh()— already handled composite keys.
oRm 0.6.2
Bug Fixes
-
Schema-qualified writes no longer break on the non-flush paths.
model$tablenameis stored as a plain"schema.table"string, and two write paths passed it directly to DBI, which quotes a character string as a single identifier — producing a relation whose name literally contains a dot. On PostgreSQL this surfaced asFailed to initialise COPY : ERROR: relation "schema.table" does not exist.-
Record$create()on its non-flush branch (the default inside a transaction, or explicitflush_record = FALSE) passed the raw string toDBI::dbAppendTable(). It now passes the name throughengine$format_tablename(), which quotes each dotted part separately. -
TableModel$create_table(overwrite = TRUE)built itsDROP TABLEstatement the same way. Because ofIF EXISTS, the drop silently missed the qualified table and “overwrite” left the old table and its data in place. It now usesformat_tablename()likedrop_table()already did.
-
oRm 0.6.1
New Features
-
Optional ellmer database-agent integration —
register_db_tools()augments an ellmer Chat with generic CRUD tools (db_read/db_create/db_update/db_delete) backed by oRm models or a reflected Engine. ellmer is in Suggests; the package loads without it.
oRm 0.6.0
Breaking Changes
-
Engine$hydrate()renamed toEngine$reflect()(andEngine$hydrate_schema()toEngine$reflect_schema()). The public verb now matches the internal reflection vocabulary it has always used (reflect_columns(),reflect_tables()) and the established ORM term for schema introspection (SQLAlchemy’sMetaData.reflect()). “Hydrate” conventionally means populating an object instance from a row, which is not what this method does — it builds a model from table metadata. There is no deprecation shim; update calls fromengine$hydrate(...)toengine$reflect(...).
New Features
-
Set-level CRUD on
TableModel— the CRUD spine is now complete and discoverable at the model level, mirroring the row-levelRecordverbs:-
Model$create(...)inserts a row (sugar overModel$record(...)$create()) and returns the persistedRecord. -
Model$update(...)issues a singleUPDATE ... WHERE. Bare expressions are the WHERE filter (likeread()); named arguments are the SET values (likecreate()), e.g.User$update(id == 1, name = "Kent", age = 35). -
Model$delete(...)issues a singleDELETE ... WHEREfrom bare-expression filters. -
update()/delete()return the affected-row count invisibly and refuse a filterless whole-table write unless.all = TRUE. Both require a primary key.
-
-
TableModel$define_relationship()method form —define_relationship()is now exposed as a model method that supplies the local model asself, making it discoverable via$-autocomplete per the verb-mirroring convention. The standalonedefine_relationship()function is retained for functional/pipe-style use and internal wiring; both share one implementation.
Bug Fixes
-
with.Engine()transactions now nest, pool, and respect read-only — three long-standing transaction bugs are fixed:-
Nesting: a
with.Engine()block opened inside another (directly or via a helper) used to hitdbBegin()on a connection already in a transaction and error out. Nested blocks now run as savepoints and commit together with the outer transaction. -
Pooling: with
use_pool = TRUE,dbBegin()was called on thePoolobject itself (and the writes scattered across checkouts). A single connection is now checked out for the life of the transaction and pinned so every operation inside the block lands on it. -
Read-only: a transaction on a
.read_onlyengine opened anyway and only failed mid-block on the first write.with.Engine()now refuses upfront.
-
Nesting: a
Double-qualification of table names in
Engine—Engine$model()andEngine$reflect()qualified the tablename before passing it toTableModel$new(), which qualified it again. Qualification now happens once, insideTableModel$new();reflect()keeps a locally qualified name only for column reflection.Single-column
read()— reads that project to a single column no longer collapse to an unnamed vector when building aRecord. Theget,one_or_none, andallmodes now usedrop = FALSEso the column name is preserved.
Documentation
- Renamed the “Hydrating Models from Existing Tables” vignette to “Reflecting Models from Existing Tables” and updated the README and
vignette("using-engine")for thereflect()/reflect_schema()naming.
oRm 0.5.0
New Features
-
PostgreSQL rich table reflection —
engine$hydrate()now captures full column metadata when using the PostgreSQL dialect:- Canonical types (e.g.
integer,text,timestamp with time zone) - Primary key flags, nullability, and column defaults
- Server-side defaults (sequences,
now(), etc.) stored asdbplyr::sql()objects and applied by the database at insert time - Foreign keys reflected as
ForeignKeyobjects, schema-qualified when the target lives in another schema
- Canonical types (e.g.
-
Schema-qualified foreign keys —
ForeignKey()now accepts cross-schema targets:- New
ref_schemaargument:ForeignKey("INTEGER", ref_schema = "other", ref_table = "users", ref_column = "id") - Three-part
referencesshorthand:ForeignKey("INTEGER", references = "other.users.id") -
render_constraint()emitsREFERENCES "other"."users" ("id")for cross-schema FKs
- New
-
Engine$hydrate_schema()— hydrate an entire schema in one call and auto-wire relationships:- Hydrates all (or a selected subset of) tables via
hydrate() - Turns every reflected
ForeignKeyinto amany_to_one/one_to_manyrelationship pair viadefine_relationship() - Accepts
tables,exclude,.schema, andwire_relationshipsarguments - Foreign keys pointing at tables outside the hydrated set emit a warning and are skipped
- Hydrates all (or a selected subset of) tables via
-
reflect_tables()generic — new dialect-dispatched function that lists the tables available in a schema:- Default implementation wraps
DBI::dbListTables() - PostgreSQL implementation queries
pg_catalog.pg_tablesfor the target schema
- Default implementation wraps
Improvements
-
Recordnow skips server-side defaults (dbplyr::sql()objects) at insert time, letting the database compute the value and relying onflush()to read it back
Documentation
- Updated README and
vignette("using-engine")to document PostgreSQL rich reflection, schema-qualified foreign keys, andhydrate_schema()
oRm 0.4.0
New Features
- Added
Method()function for attaching custom methods to models at both table and record levels- Table-level methods operate on the entire table for custom queries and bulk operations
- Record-level methods operate on individual records for instance-specific behavior
- Both method types have access to
selffor calling model/record methods
- Added automatic JSON serialization/deserialization for PostgreSQL
- JSON and JSONB columns automatically serialize R objects when writing to database
- Automatically deserialize JSON to R objects when reading from database
- Added
Engine(.read_only = TRUE)for read-only database access- Application-level guards block all INSERT, UPDATE, DELETE, CREATE TABLE, and DROP TABLE operations
- Dialect-level enforcement: SQLite uses the
SQLITE_ROopen flag; PostgreSQL setsdefault_transaction_read_only=onvia libpq; MySQL executesSET SESSION TRANSACTION READ ONLYpost-connect
- Added partial model support: a
TableModelcan declare a subset of an existing table’s columns-
read()projects results to only the declared fields, preventing undeclared columns from surfacing inRecordobjects - Useful for scoped, safe access to production tables with sensitive or irrelevant columns
-
-
create_table(overwrite = TRUE)now prompts for confirmation in interactive sessions- Pass
ask = FALSEto bypass the prompt in scripts and automated workflows
- Pass