WowSQL Ruby SDK

The official Ruby SDK for WowSQL. Provides a clean, chainable interface for all PostgREST database operations, authentication, file storage, and schema management.


Table of Contents


Requirements

  • Ruby 2.7 or higher
  • faraday >= 1.0
  • faraday-multipart >= 1.0

Installation

Add to your Gemfile:

gem 'wowsql-sdk'

Or install directly:

gem install wowsql-sdk

Quick Start

require 'wowsql'

# Initialize client with your project slug and API key
client = WOWSQL::WOWSQLClient.new("myproject", "wowsql_anon_...")

# Insert a record
user = client.table("users").create({ email: "alice@example.com", name: "Alice" })

# Query with filters
result = client.table("users")
               .select("id", "email", "name")
               .eq("is_active", true)
               .order_by("created_at", "desc")
               .limit(10)
               .get

result["data"].each { |u| puts u["email"] }

# Close when done
client.close

Client Configuration

client = WOWSQL::WOWSQLClient.new(
  "myproject",           # Project slug, full hostname, or full URL
  "wowsql_anon_...",     # Anonymous key (or service role key for privileged ops)
  base_domain: "wowsqlconnect.com",  # Default; only needed if self-hosting
  secure: true,          # Use HTTPS (default: true)
  timeout: 30,           # Request timeout in seconds (default: 30)
  verify_ssl: true       # Verify SSL certificates (default: true)
)

project_url formats accepted:

Format Description
"myproject" Appends .wowsqlconnect.com automatically
"myproject.wowsqlconnect.com" Full hostname
"https://myproject.wowsqlconnect.com" Full URL
"https://your-self-hosted-domain.com" Self-hosted instance

API Key types:

Key Prefix Purpose
wowsql_anon_... Public / client-side operations
wowsql_service_... Server-side, privileged operations (schema management, admin)

Database Operations

get — Query Records

# All records
result = client.table("products").get

# Chained query
result = client.table("products")
               .select("id", "name", "price")
               .eq("category", "electronics")
               .order_by("price", "asc")
               .limit(20)
               .offset(0)
               .get

puts result["data"]    # Array of hashes
puts result["count"]   # Records returned
puts result["total"]   # Total matching records
puts result["limit"]   # Applied limit
puts result["offset"]  # Applied offset

get_by_id — Fetch by Primary Key

user = client.table("users").get_by_id("550e8400-e29b-41d4-a716-446655440000")
puts user["email"]

create / insert — Insert a Record

product = client.table("products").create({
  name: "Widget Pro",
  price: 29.99,
  category: "tools",
  in_stock: true
})
puts product["id"]

insert is an alias for create.

bulk_insert — Insert Multiple Records

records = [
  { name: "Item A", price: 10.00 },
  { name: "Item B", price: 20.00 },
  { name: "Item C", price: 30.00 }
]
results = client.table("products").bulk_insert(records)
puts "Inserted #{results.length} records"

upsert — Insert or Update

# Inserts if not exists; updates if the id already exists
record = client.table("settings").upsert(
  { id: "user-uuid", theme: "dark", language: "en" },
  on_conflict: "id"
)

update — Update by ID

updated = client.table("users").update(
  "550e8400-e29b-41d4-a716-446655440000",
  { name: "Alice Smith", updated_at: Time.now.iso8601 }
)
puts updated["name"]

delete — Delete by ID

deleted = client.table("users").delete("550e8400-e29b-41d4-a716-446655440000")

Query Builder

All query builder methods return self and are fully chainable. Call get at the end to execute.

select — Choose Columns

# Specific columns
client.table("users").select("id", "email", "name").get

# All columns (default)
client.table("users").get

Filtering

Available operators

Method PostgREST operator Description
eq(col, val) eq Equals
neq(col, val) neq Not equals
gt(col, val) gt Greater than
gte(col, val) gte Greater than or equal
lt(col, val) lt Less than
lte(col, val) lte Less than or equal
like(col, pat) like SQL LIKE pattern
ilike(col, pat) ilike Case-insensitive LIKE
is_null(col) is.null Column is NULL
is_not_null(col) not.is.null Column is not NULL
in_list(col, arr) in.(...) Column in list
not_in(col, arr) not.in.(...) Column not in list
between(col, min, max) gte+lte Inclusive range
not_between(col, min, max) lt+gt Outside range
filter(col, op, val) any above Generic filter
or_filter(col, op, val) OR OR condition
# Chained filters (all AND by default)
result = client.table("orders")
               .gte("total", 100)
               .lte("total", 500)
               .eq("status", "shipped")
               .get

# LIKE / ILIKE
result = client.table("products")
               .ilike("name", "%widget%")
               .get

# IN list
result = client.table("users")
               .in_list("role", ["admin", "manager"])
               .get

# NULL check
result = client.table("users").is_null("deleted_at").get

# Date range
result = client.table("orders")
               .between("created_at", "2025-01-01", "2025-12-31")
               .get

order_by — Sort Results

# Single column
client.table("products").order_by("price", "asc").get
client.table("products").order_by("created_at", "desc").get

# Multiple columns
client.table("products")
      .order_by("category", "asc")
      .order_by("price", "desc")
      .get

group_by — Aggregate Groups

result = client.table("orders")
               .select("status", "sum(total)", "count(*)")
               .group_by("status")
               .get

limit / offset — Pagination

result = client.table("products").limit(20).offset(40).get

paginate — Page-Based Pagination

result = client.table("products").paginate(page: 3, per_page: 20)

puts result["data"]        # Array of records
puts result["page"]        # 3
puts result["per_page"]    # 20
puts result["total"]       # Total records matching filters
puts result["total_pages"] # Total pages

first — Single Record

user = client.table("users").eq("email", "alice@example.com").first
puts user["name"]   # nil if not found

single — Exactly One Record

begin
  user = client.table("users").eq("email", "alice@example.com").single
rescue WOWSQL::WOWSQLError => e
  puts "Not found or multiple records: #{e.message}"
end

count — Total Count

total = client.table("users").eq("is_active", true).count
puts "Active users: #{total}"

sum / avg — Aggregates

total_revenue = client.table("orders").eq("status", "completed").sum("total")
avg_price     = client.table("products").eq("category", "electronics").avg("price")

puts "Revenue: #{total_revenue}"
puts "Average price: #{avg_price}"

Authentication

auth = WOWSQL::ProjectAuthClient.new(
  "myproject",
  "wowsql_anon_...",
  base_domain: "wowsqlconnect.com",
  secure: true,
  timeout: 30,
  verify_ssl: true,
  token_storage: nil   # Optional custom token storage (implements TokenStorage)
)

sign_up

response = auth.(
  email: "alice@example.com",
  password: "SecurePass123!",
  full_name: "Alice Smith",
  user_metadata: { plan: "pro" }
)

puts response.session.access_token
puts response.user.id
puts response.user.email

sign_in

response = auth.(
  email: "alice@example.com",
  password: "SecurePass123!"
)

puts response.session.access_token
puts response.session.refresh_token

get_user

user = auth.get_user
puts user.id
puts user.email
puts user.full_name
puts user.email_verified
puts user..inspect

OAuth — Google, GitHub, etc.

Step 1: Get the authorization URL

oauth = auth.get_oauth_authorization_url(
  provider: "google",
  redirect_uri: "https://myapp.com/auth/callback"
)

# Redirect the browser to:
puts oauth["authorization_url"]

Step 2: Exchange the callback code

result = auth.exchange_oauth_callback(
  provider: "google",
  code: params[:code],
  redirect_uri: "https://myapp.com/auth/callback"
)

puts result.session.access_token
puts result.user.email

forgot_password / reset_password

# Send reset email
auth.forgot_password(email: "alice@example.com")

# Reset with token from email
auth.reset_password(
  token: "reset_token_from_email",
  new_password: "NewSecurePass456!"
)

send_otp / verify_otp

# Send OTP
auth.send_otp(email: "alice@example.com", purpose: "login")

# Verify OTP
response = auth.verify_otp(
  email: "alice@example.com",
  otp: "123456",
  purpose: "login"
)
puts response.session.access_token

Purposes: "login", "signup", "password_reset".

auth.send_magic_link(email: "alice@example.com", purpose: "login")

Purposes: "login", "signup", "email_verification".

verify_email / resend_verification

# Verify email from link token
result = auth.verify_email(token: "verification_token_from_email")
puts result["success"]

# Resend if needed
auth.resend_verification(email: "alice@example.com")

refresh_token

response = auth.refresh_token
puts response.session.access_token

change_password / update_user

# Change password
auth.change_password(
  current_password: "OldPass123!",
  new_password: "NewPass456!"
)

# Update profile
user = auth.update_user(
  full_name: "Alice Smith",
  avatar_url: "https://cdn.example.com/avatar.jpg",
  user_metadata: { bio: "Developer" }
)

logout

auth.logout

File Storage

storage = WOWSQL::WOWSQLStorage.new(
  "myproject",
  "wowsql_anon_...",
  base_domain: "wowsqlconnect.com",
  secure: true,
  timeout: 60,
  verify_ssl: true
)

create_bucket

bucket = storage.create_bucket(
  "avatars",
  public: true,
  file_size_limit: 5 * 1024 * 1024,       # 5 MB
  allowed_mime_types: ["image/jpeg", "image/png"]
)
puts bucket.name
puts bucket.public

upload / upload_from_path

# Upload from IO
File.open("photo.jpg", "rb") do |f|
  file = storage.upload("avatars", f, path: "users/alice.jpg")
  puts file.path
  puts file.size
end

# Upload from filesystem path
file = storage.upload_from_path(
  "/local/path/photo.jpg",
  bucket_name: "avatars",
  path: "users/alice.jpg"
)

list_files / download / delete_file

# List files
files = storage.list_files("avatars", prefix: "users/", limit: 50)
files.each { |f| puts "#{f.path} (#{f.size_mb.round(2)} MB)" }

# Download to memory
content = storage.download("avatars", "users/alice.jpg")

# Download to disk
storage.download_to_file("avatars", "users/alice.jpg", "/local/alice.jpg")

# Delete
storage.delete_file("avatars", "users/alice.jpg")

get_public_url

url = storage.get_public_url("avatars", "users/alice.jpg")
puts url   # https://myproject.wowsqlconnect.com/api/v1/storage/...

Schema Management

Schema operations require a service role key (wowsql_service_...).

schema = WOWSQL::WOWSQLSchema.new(
  "myproject",
  "wowsql_service_...",
  base_domain: "wowsqlconnect.com",
  secure: true
)

create_table

schema.create_table(
  "products",
  [
    { "name" => "id",         "type" => "UUID",         "auto_increment" => true },
    { "name" => "name",       "type" => "VARCHAR(255)",  "nullable" => false },
    { "name" => "price",      "type" => "DECIMAL(10,2)", "nullable" => false },
    { "name" => "category",   "type" => "VARCHAR(100)" },
    { "name" => "metadata",   "type" => "JSONB",         "default" => "'{}'" },
    { "name" => "created_at", "type" => "TIMESTAMPTZ",   "default" => "CURRENT_TIMESTAMP" }
  ],
  primary_key: "id",
  indexes: ["category", "name"]
)

add_column / drop_column / rename_column

# Add
schema.add_column("products", "sku", "VARCHAR(50)", nullable: true)

# Drop
schema.drop_column("products", "old_field")

# Rename
schema.rename_column("products", "sku", "product_sku")

# Modify type / nullability / default
schema.modify_column("products", "price", column_type: "NUMERIC(12,2)", nullable: false)

drop_table / execute_sql

# Drop table (irreversible)
schema.drop_table("products", cascade: false)

# Execute raw DDL SQL
schema.execute_sql("CREATE INDEX idx_products_category ON products (category)")
schema.execute_sql("ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ")

Error Handling

All SDK errors are subclasses of WOWSQL::WOWSQLError.

begin
  result = client.table("orders").get_by_id("some-id")
rescue WOWSQL::WOWSQLError => e
  puts e.message      # Human-readable error message
  puts e.status_code  # HTTP status code (e.g., 400, 401, 403, 404, 500)
  puts e.response     # Raw response body hash
end

Storage-specific errors:

begin
  storage.upload("avatars", large_file, path: "big.mov")
rescue WOWSQL::StorageLimitExceededError => e
  puts "File too large: #{e.message}"
rescue WOWSQL::StorageError => e
  puts "Storage error: #{e.message}"
end

Schema-specific errors:

begin
  schema.drop_table("important_table")
rescue WOWSQL::SchemaPermissionError => e
  puts "Permission denied — use a service role key"
rescue WOWSQL::WOWSQLError => e
  puts "Schema error: #{e.message}"
end

Response Format

All get and query builder calls return a consistent hash:

{
  "data"   => [...],    # Array of record hashes
  "count"  => 10,       # Number of records in this response
  "total"  => 120,      # Total matching records (from Content-Range)
  "limit"  => 20,       # Applied limit
  "offset" => 0         # Applied offset
}

Single-record operations (create, update, delete, get_by_id, upsert) return a plain Hash representing the record.

Pagination (paginate) returns:

{
  "data"        => [...],
  "page"        => 2,
  "per_page"    => 20,
  "total"       => 120,
  "total_pages" => 6
}

License

MIT License — see LICENSE for details.