@@ -1298,6 +1298,108 @@ mix ecto.migrate # Run migrations
12981298mix ecto.rollback # Rollback last migration
12991299```
13001300
1301+ #### RANDOM ROWID Support (libSQL Extension)
1302+
1303+ For security and privacy, use RANDOM ROWID to generate pseudorandom row IDs instead of sequential integers:
1304+
1305+ ``` elixir
1306+ # Create table with random row IDs (prevents ID enumeration attacks)
1307+ defmodule MyApp .Repo .Migrations .CreateSessions do
1308+ use Ecto .Migration
1309+
1310+ def change do
1311+ create table (:sessions , options: [random_rowid: true ]) do
1312+ add :token , :string , null: false
1313+ add :user_id , references (:users , on_delete: :delete_all )
1314+ add :expires_at , :utc_datetime
1315+
1316+ timestamps ()
1317+ end
1318+
1319+ create unique_index (:sessions , [:token ])
1320+ end
1321+ end
1322+ ```
1323+
1324+ ** Benefits:**
1325+ - ** Security** : Prevents ID enumeration attacks (guessing valid IDs)
1326+ - ** Privacy** : Doesn't leak business metrics through sequential IDs
1327+ - ** Unpredictability** : Row IDs are pseudorandom, not sequential
1328+
1329+ ** Usage:**
1330+ ``` elixir
1331+ # Basic usage
1332+ create table (:sessions , options: [random_rowid: true ]) do
1333+ add :token , :string
1334+ end
1335+
1336+ # With composite primary key
1337+ create table (:audit_log , options: [random_rowid: true ]) do
1338+ add :user_id , :integer , primary_key: true
1339+ add :action_id , :integer , primary_key: true
1340+ add :timestamp , :integer
1341+ end
1342+
1343+ # With IF NOT EXISTS
1344+ create_if_not_exists table (:sessions , options: [random_rowid: true ]) do
1345+ add :token , :string
1346+ end
1347+ ```
1348+
1349+ ** Restrictions:**
1350+ - Mutually exclusive with WITHOUT ROWID (per libSQL specification)
1351+ - Mutually exclusive with AUTOINCREMENT (per libSQL specification)
1352+ - LibSQL extension - not available in standard SQLite
1353+
1354+ ** SQL Output:**
1355+ ``` sql
1356+ CREATE TABLE sessions (...) RANDOM ROWID
1357+ ```
1358+
1359+ #### ALTER COLUMN Support (libSQL Extension)
1360+
1361+ LibSQL supports modifying column attributes with ALTER COLUMN (not available in standard SQLite):
1362+
1363+ ``` elixir
1364+ defmodule MyApp .Repo .Migrations .ModifyUserColumns do
1365+ use Ecto .Migration
1366+
1367+ def change do
1368+ alter table (:users ) do
1369+ # Change column type
1370+ modify :age , :string , default: " 0"
1371+
1372+ # Add NOT NULL constraint
1373+ modify :email , :string , null: false
1374+
1375+ # Add DEFAULT value
1376+ modify :status , :string , default: " active"
1377+
1378+ # Add foreign key reference
1379+ modify :team_id , references (:teams , on_delete: :nilify_all )
1380+ end
1381+ end
1382+ end
1383+ ```
1384+
1385+ ** Supported Modifications:**
1386+ - Type affinity changes (` :integer ` → ` :string ` , etc.)
1387+ - NOT NULL constraints
1388+ - DEFAULT values
1389+ - CHECK constraints
1390+ - REFERENCES (foreign keys)
1391+
1392+ ** Important Notes:**
1393+ - Changes only apply to ** new or updated rows**
1394+ - Existing data is ** not revalidated** or modified
1395+ - This is a ** libSQL extension** - not available in standard SQLite
1396+
1397+ ** SQL Output:**
1398+ ``` sql
1399+ ALTER TABLE users ALTER COLUMN age TO age TEXT DEFAULT ' 0'
1400+ ALTER TABLE users ALTER COLUMN email TO email TEXT NOT NULL
1401+ ```
1402+
13011403### Basic Queries
13021404
13031405#### Insert
@@ -1719,33 +1821,61 @@ Ecto types map to SQLite types as follows:
17191821
17201822### Ecto Migration Notes
17211823
1722- Most Ecto migrations work perfectly. SQLite limitations :
1824+ Most Ecto migrations work perfectly. LibSQL provides extensions beyond standard SQLite :
17231825
17241826``` elixir
1725- # ✅ SUPPORTED
1726- create table (:users )
1727- alter table (:users ) do: add :field , :type
1728- drop table (:users )
1729- create index (:users , [:email ])
1730- rename table (:old ), to: table (:new )
1731- rename table (:users ), :old_field , to: :new_field
1827+ # ✅ FULLY SUPPORTED
1828+ create table (:users ) # CREATE TABLE
1829+ create table (:sessions , options: [random_rowid: true ]) # RANDOM ROWID (libSQL extension)
1830+ alter table (:users ) do: add :field , :type # ADD COLUMN
1831+ alter table (:users ) do: modify :field , :new_type # ALTER COLUMN (libSQL extension)
1832+ alter table (:users ) do: remove :field # DROP COLUMN (libSQL/SQLite 3.35.0+)
1833+ drop table (:users ) # DROP TABLE
1834+ create index (:users , [:email ]) # CREATE INDEX
1835+ rename table (:old ), to: table (:new ) # RENAME TABLE
1836+ rename table (:users ), :old_field , to: :new_field # RENAME COLUMN
1837+
1838+ # ⚠️ LIBSQL EXTENSIONS (not in standard SQLite)
1839+ alter table (:users ) do: modify :age , :string # ALTER COLUMN - libSQL only
1840+ create table (:sessions , options: [random_rowid: true ]) # RANDOM ROWID - libSQL only
1841+ ```
1842+
1843+ ** Important Notes:**
1844+
1845+ 1 . ** ALTER COLUMN** is a libSQL extension (not available in standard SQLite)
1846+ - Supported operations: type changes, NOT NULL, DEFAULT, CHECK, REFERENCES
1847+ - Changes only apply to new/updated rows; existing data is not revalidated
17321848
1733- # ❌ NOT SUPPORTED
1734- alter table (:users ) do: modify :field , :new_type # Can't change column type
1735- alter table (:users ) do: remove :field # Can't drop column (SQLite < 3.35.0)
1849+ 2 . ** DROP COLUMN** requires SQLite 3.35.0+ or libSQL
1850+ - Cannot drop PRIMARY KEY columns, UNIQUE columns, or referenced columns
17361851
1737- # Workaround: Recreate table
1852+ 3 . ** RANDOM ROWID** is a libSQL extension for security/privacy
1853+ - Prevents ID enumeration attacks
1854+ - Mutually exclusive with WITHOUT ROWID and AUTOINCREMENT
1855+
1856+ ** Standard SQLite Workaround (if not using libSQL's ALTER COLUMN):**
1857+
1858+ If you need to modify columns on standard SQLite (without libSQL's extensions), recreate the table:
1859+
1860+ ``` elixir
17381861defmodule MyApp .Repo .Migrations .ChangeUserAge do
17391862 use Ecto .Migration
17401863
17411864 def up do
17421865 create table (:users_new ) do
1743- # Define new schema
1866+ add :id , :integer , primary_key: true
1867+ add :name , :string
1868+ add :email , :string
1869+ add :age , :string # Changed from :integer
1870+ timestamps ()
17441871 end
17451872
1746- execute " INSERT INTO users_new SELECT * FROM users"
1873+ execute " INSERT INTO users_new (id, name, email, age, inserted_at, updated_at) SELECT id, name, email, CAST(age AS TEXT), inserted_at, updated_at FROM users"
17471874 drop table (:users )
17481875 rename table (:users_new ), to: table (:users )
1876+
1877+ # Recreate indexes
1878+ create unique_index (:users , [:email ])
17491879 end
17501880end
17511881```
0 commit comments