or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-cursors.mdbatch-operations.mdconnection-pooling.mdconnections-cursors.mderror-handling.mdindex.mdreplication.mdsql-composition.mdtimezone-support.mdtype-adaptation.md

sql-composition.mddocs/

0

# SQL Composition

1

2

Safe SQL query construction with automatic quoting, parameter placeholders, and composable SQL elements to prevent SQL injection vulnerabilities while building dynamic queries programmatically.

3

4

## Capabilities

5

6

### SQL Building Classes

7

8

Composable SQL elements that can be combined to build complex queries safely with automatic escaping and quoting.

9

10

```python { .api }

11

class Composable:

12

"""Abstract base for SQL composition objects."""

13

14

def __init__(self, wrapped):

15

"""Initialize with wrapped object."""

16

17

def as_string(self, context):

18

"""Get string representation (abstract method)."""

19

20

def __add__(self, other):

21

"""Concatenate with another Composable."""

22

23

def __mul__(self, n):

24

"""Repeat n times."""

25

26

def __eq__(self, other):

27

"""Equality comparison."""

28

29

def __ne__(self, other):

30

"""Inequality comparison."""

31

32

class Composed(Composable):

33

"""Sequence of Composable objects."""

34

35

def __init__(self, seq):

36

"""Initialize with sequence of Composables."""

37

38

@property

39

def seq(self):

40

"""Property returning content list."""

41

42

def as_string(self, context):

43

"""Join all elements."""

44

45

def __iter__(self):

46

"""Iterator over elements."""

47

48

def __add__(self, other):

49

"""Concatenation."""

50

51

def join(self, joiner):

52

"""

53

Join elements with separator.

54

55

Parameters:

56

- joiner (Composable): Separator to join with

57

58

Returns:

59

Composed: New composed object with joined elements

60

"""

61

62

class SQL(Composable):

63

"""Raw SQL snippet."""

64

65

def __init__(self, string):

66

"""

67

Initialize with SQL string.

68

69

Parameters:

70

- string (str): Raw SQL text

71

"""

72

73

@property

74

def string(self):

75

"""Property returning wrapped string."""

76

77

def as_string(self, context):

78

"""Return string as-is."""

79

80

def format(self, *args, **kwargs):

81

"""

82

Format with placeholders.

83

84

Parameters:

85

- *args: Positional Composable arguments

86

- **kwargs: Named Composable arguments

87

88

Returns:

89

Composed: Formatted SQL composition

90

"""

91

92

def join(self, seq):

93

"""

94

Join sequence with SQL string.

95

96

Parameters:

97

- seq (sequence): Sequence of Composables to join

98

99

Returns:

100

Composed: Joined composition

101

"""

102

103

class Identifier(Composable):

104

"""SQL identifier (quoted)."""

105

106

def __init__(self, *strings):

107

"""

108

Initialize with identifier parts.

109

110

Parameters:

111

- *strings: Identifier components (schema, table, column, etc.)

112

"""

113

114

@property

115

def strings(self):

116

"""Property returning tuple of strings."""

117

118

@property

119

def string(self):

120

"""Property for single string (raises if multiple)."""

121

122

def as_string(self, context):

123

"""Return quoted identifier."""

124

125

class Literal(Composable):

126

"""SQL literal value."""

127

128

@property

129

def wrapped(self):

130

"""Property returning wrapped object."""

131

132

def as_string(self, context):

133

"""Return adapted/quoted value."""

134

135

class Placeholder(Composable):

136

"""Parameter placeholder."""

137

138

def __init__(self, name=None):

139

"""

140

Initialize with optional name.

141

142

Parameters:

143

- name (str, optional): Placeholder name for named parameters

144

"""

145

146

@property

147

def name(self):

148

"""Property returning placeholder name."""

149

150

def as_string(self, context):

151

"""Return %s or %(name)s."""

152

```

153

154

**Usage Example:**

155

156

```python

157

from psycopg2 import sql

158

159

# Basic SQL composition

160

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

161

fields=sql.SQL(', ').join([

162

sql.Identifier('id'),

163

sql.Identifier('name'),

164

sql.Identifier('email')

165

]),

166

table=sql.Identifier('users'),

167

condition=sql.SQL("{} > {}").format(

168

sql.Identifier('age'),

169

sql.Literal(25)

170

)

171

)

172

173

print(query.as_string(conn))

174

# SELECT "id", "name", "email" FROM "users" WHERE "age" > 25

175

176

# Dynamic WHERE clauses

177

conditions = []

178

params = []

179

180

if name_filter:

181

conditions.append(sql.SQL("{} ILIKE {}").format(

182

sql.Identifier('name'),

183

sql.Placeholder()

184

))

185

params.append(f"%{name_filter}%")

186

187

if age_min:

188

conditions.append(sql.SQL("{} >= {}").format(

189

sql.Identifier('age'),

190

sql.Placeholder()

191

))

192

params.append(age_min)

193

194

if conditions:

195

where_clause = sql.SQL(" WHERE ").join([

196

sql.SQL(""),

197

sql.SQL(" AND ").join(conditions)

198

])

199

else:

200

where_clause = sql.SQL("")

201

202

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

203

table=sql.Identifier('users'),

204

where=where_clause

205

)

206

207

# Execute with parameters

208

cur.execute(query, params)

209

```

210

211

### SQL Constants

212

213

Pre-defined SQL constants for common values.

214

215

```python { .api }

216

NULL = SQL("NULL") # SQL NULL constant

217

DEFAULT = SQL("DEFAULT") # SQL DEFAULT constant

218

```

219

220

**Usage Example:**

221

222

```python

223

from psycopg2 import sql

224

225

# Using SQL constants

226

insert_query = sql.SQL("INSERT INTO {table} ({columns}) VALUES ({values})").format(

227

table=sql.Identifier('users'),

228

columns=sql.SQL(', ').join([

229

sql.Identifier('name'),

230

sql.Identifier('email'),

231

sql.Identifier('created_at')

232

]),

233

values=sql.SQL(', ').join([

234

sql.Placeholder('name'),

235

sql.Placeholder('email'),

236

sql.DEFAULT # Use DEFAULT for created_at timestamp

237

])

238

)

239

240

cur.execute(insert_query, {'name': 'Alice', 'email': 'alice@example.com'})

241

```

242

243

### Safe Dynamic Queries

244

245

Building queries dynamically while preventing SQL injection attacks.

246

247

**Usage Example:**

248

249

```python

250

from psycopg2 import sql

251

import psycopg2

252

253

def build_select_query(table, columns=None, conditions=None, order_by=None, limit=None):

254

"""Build a safe SELECT query dynamically."""

255

256

# Default to all columns

257

if columns:

258

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

259

else:

260

fields = sql.SQL('*')

261

262

# Base query

263

query_parts = [

264

sql.SQL("SELECT"),

265

fields,

266

sql.SQL("FROM"),

267

sql.Identifier(table)

268

]

269

params = []

270

271

# Add WHERE clause

272

if conditions:

273

where_conditions = []

274

for column, operator, value in conditions:

275

where_conditions.append(

276

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

277

sql.Identifier(column),

278

sql.SQL(operator),

279

sql.Placeholder()

280

)

281

)

282

params.append(value)

283

284

query_parts.extend([

285

sql.SQL("WHERE"),

286

sql.SQL(" AND ").join(where_conditions)

287

])

288

289

# Add ORDER BY

290

if order_by:

291

order_fields = []

292

for col_spec in order_by:

293

if isinstance(col_spec, tuple):

294

col, direction = col_spec

295

order_fields.append(

296

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

297

sql.Identifier(col),

298

sql.SQL(direction)

299

)

300

)

301

else:

302

order_fields.append(sql.Identifier(col_spec))

303

304

query_parts.extend([

305

sql.SQL("ORDER BY"),

306

sql.SQL(", ").join(order_fields)

307

])

308

309

# Add LIMIT

310

if limit:

311

query_parts.extend([

312

sql.SQL("LIMIT"),

313

sql.Placeholder()

314

])

315

params.append(limit)

316

317

return sql.SQL(" ").join(query_parts), params

318

319

# Usage

320

conn = psycopg2.connect("host=localhost dbname=mydb user=myuser")

321

322

query, params = build_select_query(

323

table='users',

324

columns=['id', 'name', 'email'],

325

conditions=[

326

('age', '>=', 18),

327

('status', '=', 'active')

328

],

329

order_by=[('name', 'ASC'), ('id', 'DESC')],

330

limit=50

331

)

332

333

with conn.cursor() as cur:

334

cur.execute(query, params)

335

results = cur.fetchall()

336

337

conn.close()

338

```

339

340

### Table and Column Name Safety

341

342

Safe handling of dynamic table and column names that come from user input or configuration.

343

344

**Usage Example:**

345

346

```python

347

from psycopg2 import sql

348

349

def safe_table_query(table_name, user_columns, user_input_table):

350

"""Safely handle user-provided table and column names."""

351

352

# Validate table name (you should have your own validation)

353

allowed_tables = ['users', 'products', 'orders']

354

if user_input_table not in allowed_tables:

355

raise ValueError(f"Table {user_input_table} not allowed")

356

357

# Validate column names

358

allowed_columns = ['id', 'name', 'email', 'created_at']

359

for col in user_columns:

360

if col not in allowed_columns:

361

raise ValueError(f"Column {col} not allowed")

362

363

# Build safe query

364

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

365

columns=sql.SQL(', ').join(sql.Identifier(col) for col in user_columns),

366

table=sql.Identifier(user_input_table)

367

)

368

369

return query

370

371

# Usage with validation

372

try:

373

query = safe_table_query('users', ['id', 'name'], 'users')

374

cur.execute(query)

375

results = cur.fetchall()

376

except ValueError as e:

377

print(f"Security error: {e}")

378

```

379

380

### Advanced Composition Patterns

381

382

Complex query building patterns for advanced use cases.

383

384

**Usage Example:**

385

386

```python

387

from psycopg2 import sql

388

389

def build_upsert_query(table, conflict_columns, update_columns, insert_columns=None):

390

"""Build PostgreSQL UPSERT query using ON CONFLICT."""

391

392

if insert_columns is None:

393

insert_columns = update_columns

394

395

# Build INSERT part

396

insert_query = sql.SQL("INSERT INTO {table} ({columns}) VALUES ({placeholders})").format(

397

table=sql.Identifier(table),

398

columns=sql.SQL(', ').join(sql.Identifier(col) for col in insert_columns),

399

placeholders=sql.SQL(', ').join(sql.Placeholder() for _ in insert_columns)

400

)

401

402

# Build ON CONFLICT part

403

conflict_clause = sql.SQL("ON CONFLICT ({conflict_cols}) DO UPDATE SET {updates}").format(

404

conflict_cols=sql.SQL(', ').join(sql.Identifier(col) for col in conflict_columns),

405

updates=sql.SQL(', ').join(

406

sql.SQL("{col} = EXCLUDED.{col}").format(col=sql.Identifier(col))

407

for col in update_columns

408

)

409

)

410

411

# Combine

412

return sql.SQL(" ").join([insert_query, conflict_clause])

413

414

# Usage

415

upsert_query = build_upsert_query(

416

table='users',

417

conflict_columns=['email'],

418

update_columns=['name', 'updated_at'],

419

insert_columns=['email', 'name', 'created_at', 'updated_at']

420

)

421

422

params = ['user@example.com', 'Updated Name', '2023-01-01', '2023-01-02']

423

cur.execute(upsert_query, params)

424

425

# Complex joins with dynamic conditions

426

def build_join_query(base_table, joins, conditions=None):

427

"""Build query with multiple joins."""

428

429

query_parts = [

430

sql.SQL("SELECT * FROM"),

431

sql.Identifier(base_table)

432

]

433

434

# Add joins

435

for join_type, join_table, on_condition in joins:

436

query_parts.extend([

437

sql.SQL(join_type),

438

sql.Identifier(join_table),

439

sql.SQL("ON"),

440

sql.SQL(on_condition)

441

])

442

443

# Add WHERE conditions

444

if conditions:

445

query_parts.extend([

446

sql.SQL("WHERE"),

447

sql.SQL(" AND ").join(sql.SQL(cond) for cond in conditions)

448

])

449

450

return sql.SQL(" ").join(query_parts)

451

452

# Usage

453

join_query = build_join_query(

454

base_table='users',

455

joins=[

456

('LEFT JOIN', 'profiles', 'users.id = profiles.user_id'),

457

('INNER JOIN', 'organizations', 'users.org_id = organizations.id')

458

],

459

conditions=['users.active = true', 'organizations.status = \'active\'']

460

)

461

```

462

463

## Types

464

465

### Composable Base Types

466

467

```python { .api }

468

class Composable:

469

"""Base class for all SQL composition objects."""

470

471

def as_string(self, context) -> str:

472

"""Get SQL string representation."""

473

474

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

475

"""Concatenate composables."""

476

477

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

478

"""Repeat composable n times."""

479

480

def __eq__(self, other) -> bool:

481

"""Equality comparison."""

482

483

class Composed(Composable):

484

"""Sequence of multiple composables."""

485

486

seq: list # List of composable elements

487

488

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

489

"""Join elements with separator."""

490

491

class SQL(Composable):

492

"""Raw SQL text."""

493

494

string: str # The SQL string

495

496

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

497

"""Format with placeholders."""

498

499

def join(self, seq) -> 'Composed':

500

"""Join sequence with this SQL."""

501

502

class Identifier(Composable):

503

"""Quoted SQL identifier."""

504

505

strings: tuple[str, ...] # Identifier components

506

string: str # Single string (if only one component)

507

508

class Literal(Composable):

509

"""SQL literal value."""

510

511

wrapped: Any # The wrapped Python value

512

513

class Placeholder(Composable):

514

"""Parameter placeholder."""

515

516

name: str | None # Placeholder name (None for positional)

517

```

518

519

### SQL Constants

520

521

```python { .api }

522

NULL: SQL # SQL("NULL")

523

DEFAULT: SQL # SQL("DEFAULT")

524

```