or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-operations.mdconnections.mdcursors.mderror-handling.mdindex.mdrow-factories.mdsql-composition.mdtype-system.md

sql-composition.mddocs/

0

# Safe SQL Composition

1

2

SQL query building with automatic escaping, identifier quoting, and parameter placeholder management to prevent SQL injection while maintaining readability and flexibility.

3

4

## Capabilities

5

6

### SQL String Composition

7

8

Build complex SQL queries safely using template strings with format placeholders and automatic escaping.

9

10

```python { .api }

11

class SQL:

12

def __init__(self, template: str):

13

"""

14

Create SQL template string.

15

16

Args:

17

template: SQL template with format placeholders like {table}, {field}

18

Must be a string literal for security

19

"""

20

21

def format(self, *args, **kwargs) -> Composed:

22

"""

23

Format SQL template with provided arguments.

24

25

Args:

26

*args: Positional format arguments

27

**kwargs: Keyword format arguments

28

29

Returns:

30

Composed SQL object ready for execution

31

"""

32

33

def join(self, seq: Iterable[Composable]) -> Composed:

34

"""

35

Join sequence of composable objects with this SQL as separator.

36

37

Args:

38

seq: Sequence of Composable objects to join

39

40

Returns:

41

Composed SQL object with joined elements

42

"""

43

44

def as_string(self, context: AdaptContext | None = None) -> str:

45

"""

46

Convert to SQL string representation.

47

48

Args:

49

context: Adaptation context for custom formatting

50

51

Returns:

52

SQL string ready for database execution

53

"""

54

```

55

56

### Identifier Management

57

58

Safely handle database identifiers (table names, column names, schema names) with automatic quoting and validation.

59

60

```python { .api }

61

class Identifier:

62

def __init__(self, *names: str):

63

"""

64

Create database identifier with automatic quoting.

65

66

Args:

67

*names: Identifier parts (e.g., schema, table, column)

68

"""

69

70

def as_string(self, context: AdaptContext | None = None) -> str:

71

"""

72

Convert to properly quoted identifier string.

73

74

Returns:

75

Quoted identifier suitable for SQL queries

76

"""

77

```

78

79

#### Usage Examples

80

81

```python

82

from psycopg import sql

83

84

# Simple identifier

85

table_name = sql.Identifier("users")

86

# Produces: "users"

87

88

# Qualified identifier

89

qualified = sql.Identifier("public", "users", "name")

90

# Produces: "public"."users"."name"

91

92

# With special characters

93

special_table = sql.Identifier("user-data")

94

# Produces: "user-data"

95

```

96

97

### Literal Value Handling

98

99

Convert Python objects to properly escaped SQL literals with automatic type detection and formatting.

100

101

```python { .api }

102

class Literal:

103

def __init__(self, obj: Any):

104

"""

105

Create SQL literal from Python object.

106

107

Args:

108

obj: Python object to convert to SQL literal

109

"""

110

111

def as_string(self, context: AdaptContext | None = None) -> str:

112

"""

113

Convert to properly escaped SQL literal string.

114

115

Args:

116

context: Adaptation context for custom type handling

117

118

Returns:

119

Escaped SQL literal string

120

"""

121

```

122

123

#### Usage Examples

124

125

```python

126

# String literal

127

name_literal = sql.Literal("John's Data")

128

# Produces: 'John''s Data'

129

130

# Number literal

131

age_literal = sql.Literal(25)

132

# Produces: 25

133

134

# Date literal

135

import datetime

136

date_literal = sql.Literal(datetime.date(2023, 12, 25))

137

# Produces: '2023-12-25'::date

138

139

# None/NULL literal

140

null_literal = sql.Literal(None)

141

# Produces: NULL

142

```

143

144

### Parameter Placeholders

145

146

Create named and positional parameter placeholders for prepared statements and dynamic queries.

147

148

```python { .api }

149

class Placeholder:

150

def __init__(self, name: str | None = None):

151

"""

152

Create parameter placeholder.

153

154

Args:

155

name: Named placeholder identifier (None for positional)

156

"""

157

158

def as_string(self, context: AdaptContext | None = None) -> str:

159

"""

160

Convert to parameter placeholder string.

161

162

Returns:

163

Parameter placeholder (%s, %(name)s, etc.)

164

"""

165

```

166

167

#### Usage Examples

168

169

```python

170

# Positional placeholder

171

pos_placeholder = sql.Placeholder()

172

# Produces: %s

173

174

# Named placeholder

175

named_placeholder = sql.Placeholder("user_id")

176

# Produces: %(user_id)s

177

```

178

179

### Composed SQL Objects

180

181

Container for multiple SQL components that can be combined and manipulated as a single unit.

182

183

```python { .api }

184

class Composed:

185

def __init__(self, seq: Iterable[Composable]):

186

"""

187

Create composed SQL object from sequence of components.

188

189

Args:

190

seq: Sequence of Composable SQL components

191

"""

192

193

def join(self, joiner: Composable) -> Composed:

194

"""

195

Join components with specified joiner.

196

197

Args:

198

joiner: Composable object to use as separator

199

200

Returns:

201

New Composed object with joined components

202

"""

203

204

def as_string(self, context: AdaptContext | None = None) -> str:

205

"""Convert all components to single SQL string"""

206

```

207

208

### Base Composable Interface

209

210

Abstract base class providing common functionality for all SQL composition objects.

211

212

```python { .api }

213

from abc import ABC, abstractmethod

214

215

class Composable(ABC):

216

@abstractmethod

217

def as_string(self, context: AdaptContext | None = None) -> str:

218

"""Convert to SQL string representation"""

219

220

def __add__(self, other: Composable) -> Composed:

221

"""Concatenate with another composable object (+)"""

222

223

def __mul__(self, n: int) -> Composed:

224

"""Repeat this object n times (*)"""

225

226

def __rmul__(self, n: int) -> Composed:

227

"""Repeat this object n times (n * obj)"""

228

```

229

230

### Utility Functions

231

232

Standalone functions for common SQL composition tasks.

233

234

```python { .api }

235

def quote(obj: Any, context: AdaptContext | None = None) -> str:

236

"""

237

Convert Python object to quoted SQL string.

238

239

Args:

240

obj: Python object to quote

241

context: Adaptation context for custom handling

242

243

Returns:

244

Quoted SQL string

245

246

Note:

247

This function is less efficient than using Literal objects

248

as it doesn't cache adaptation rules.

249

"""

250

```

251

252

## Advanced Usage Examples

253

254

### Dynamic Query Building

255

256

```python

257

from psycopg import sql

258

259

# Build SELECT query dynamically

260

def build_select(table, columns, where_conditions=None):

261

query = sql.SQL("SELECT {fields} FROM {table}").format(

262

fields=sql.SQL(", ").join(sql.Identifier(col) for col in columns),

263

table=sql.Identifier(table)

264

)

265

266

if where_conditions:

267

where_parts = []

268

for column, value in where_conditions.items():

269

where_parts.append(

270

sql.SQL("{field} = {value}").format(

271

field=sql.Identifier(column),

272

value=sql.Placeholder(column)

273

)

274

)

275

276

query += sql.SQL(" WHERE ") + sql.SQL(" AND ").join(where_parts)

277

278

return query

279

280

# Usage

281

query = build_select(

282

"users",

283

["id", "name", "email"],

284

{"active": True, "age": 25}

285

)

286

# Produces: SELECT "id", "name", "email" FROM "users" WHERE "active" = %(active)s AND "age" = %(age)s

287

288

cursor.execute(query, {"active": True, "age": 25})

289

```

290

291

### Complex INSERT Statements

292

293

```python

294

# Dynamic INSERT with multiple rows

295

def build_insert(table, columns, num_rows):

296

placeholders = sql.SQL("({})").format(

297

sql.SQL(", ").join(sql.Placeholder() for _ in columns)

298

)

299

300

values_list = sql.SQL(", ").join(placeholders for _ in range(num_rows))

301

302

query = sql.SQL("INSERT INTO {table} ({fields}) VALUES {values}").format(

303

table=sql.Identifier(table),

304

fields=sql.SQL(", ").join(sql.Identifier(col) for col in columns),

305

values=values_list

306

)

307

return query

308

309

# Insert multiple users

310

columns = ["name", "email", "age"]

311

query = build_insert("users", columns, 3)

312

cursor.execute(query, ("Alice", "alice@example.com", 30,

313

"Bob", "bob@example.com", 25,

314

"Charlie", "charlie@example.com", 35))

315

```

316

317

### UPDATE with Dynamic SET Clause

318

319

```python

320

def build_update(table, updates, where_conditions):

321

set_parts = []

322

for column, value in updates.items():

323

set_parts.append(

324

sql.SQL("{field} = {placeholder}").format(

325

field=sql.Identifier(column),

326

placeholder=sql.Placeholder(column)

327

)

328

)

329

330

query = sql.SQL("UPDATE {table} SET {sets}").format(

331

table=sql.Identifier(table),

332

sets=sql.SQL(", ").join(set_parts)

333

)

334

335

if where_conditions:

336

where_parts = []

337

for column, value in where_conditions.items():

338

where_key = f"where_{column}"

339

where_parts.append(

340

sql.SQL("{field} = {placeholder}").format(

341

field=sql.Identifier(column),

342

placeholder=sql.Placeholder(where_key)

343

)

344

)

345

query += sql.SQL(" WHERE ") + sql.SQL(" AND ").join(where_parts)

346

347

return query

348

349

# Update user data

350

updates = {"name": "John Smith", "email": "john.smith@example.com"}

351

where = {"id": 123}

352

query = build_update("users", updates, where)

353

354

params = {**updates, "where_id": 123}

355

cursor.execute(query, params)

356

```

357

358

### Schema and Table Management

359

360

```python

361

# Create table with dynamic schema

362

def create_table_query(schema, table, columns):

363

"""

364

columns: dict of {column_name: column_definition}

365

"""

366

table_ident = sql.Identifier(schema, table) if schema else sql.Identifier(table)

367

368

column_defs = []

369

for name, definition in columns.items():

370

column_defs.append(

371

sql.SQL("{name} {definition}").format(

372

name=sql.Identifier(name),

373

definition=sql.SQL(definition)

374

)

375

)

376

377

query = sql.SQL("CREATE TABLE {table} ({columns})").format(

378

table=table_ident,

379

columns=sql.SQL(", ").join(column_defs)

380

)

381

return query

382

383

# Usage

384

columns = {

385

"id": "SERIAL PRIMARY KEY",

386

"name": "VARCHAR(100) NOT NULL",

387

"email": "VARCHAR(255) UNIQUE",

388

"created_at": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"

389

}

390

391

query = create_table_query("public", "users", columns)

392

cursor.execute(query)

393

```

394

395

### Handling Complex WHERE Clauses

396

397

```python

398

def build_complex_where(conditions):

399

"""

400

conditions: dict with operators

401

{

402

"name": ("LIKE", "%john%"),

403

"age": (">=", 18),

404

"status": ("IN", ["active", "pending"])

405

}

406

"""

407

where_parts = []

408

params = {}

409

410

for i, (column, (operator, value)) in enumerate(conditions.items()):

411

param_name = f"param_{i}"

412

413

if operator.upper() == "IN":

414

# Handle IN clause with list

415

placeholders = sql.SQL("({})").format(

416

sql.SQL(", ").join(sql.Placeholder(f"{param_name}_{j}")

417

for j in range(len(value)))

418

)

419

where_parts.append(

420

sql.SQL("{field} IN {placeholders}").format(

421

field=sql.Identifier(column),

422

placeholders=placeholders

423

)

424

)

425

for j, item in enumerate(value):

426

params[f"{param_name}_{j}"] = item

427

else:

428

where_parts.append(

429

sql.SQL("{field} {op} {placeholder}").format(

430

field=sql.Identifier(column),

431

op=sql.SQL(operator),

432

placeholder=sql.Placeholder(param_name)

433

)

434

)

435

params[param_name] = value

436

437

where_clause = sql.SQL(" AND ").join(where_parts)

438

return where_clause, params

439

440

# Usage

441

conditions = {

442

"name": ("LIKE", "%john%"),

443

"age": (">=", 18),

444

"status": ("IN", ["active", "pending"])

445

}

446

447

where_clause, params = build_complex_where(conditions)

448

query = sql.SQL("SELECT * FROM {table} WHERE {where}").format(

449

table=sql.Identifier("users"),

450

where=where_clause

451

)

452

453

cursor.execute(query, params)

454

```

455

456

## Security Benefits

457

458

The SQL composition system provides several security advantages:

459

460

1. **SQL Injection Prevention**: All user input is properly escaped and parameterized

461

2. **Identifier Safety**: Table and column names are automatically quoted to handle special characters

462

3. **Type Safety**: Automatic type detection and proper formatting for different data types

463

4. **Validation**: Invalid SQL constructs are caught at composition time rather than execution time

464

465

## Performance Considerations

466

467

- SQL composition objects are lightweight and can be reused

468

- Complex queries should be built once and reused with different parameters

469

- Use `sql.Placeholder()` for repeated parameter patterns

470

- Consider caching composed queries for frequently used patterns