Build a Chainable Python SQL Query Builder with Where, Alias and Join Support
Company: Pylon Lending
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Technical Screen
Implement a small ORM-style query builder in Python. An existing test suite drives it through a fluent API and compares the string returned by `render()` with the SQL it expects. The tests use calls like these:
```python
o.table("users").select("id", "email")
o.table("products").select("id", "upc", "volume").where(
o.cond("volume", ">", 300),
o.cond("upc", "=", "1337"),
)
o.table("products").select("product_id", "price").alias("p")
```
Queries can also be joined:
```python
users = o.table("users").select("user_id", "email").alias("u")
apps = o.table("applications").select("loan_id").alias("app")
users.join(apps, using="user_id")
```
Here `o` is the entry point the tests use (a module or an object). Implement `o.table(name)`, `o.cond(column, operator, value)`, and a query object with `select`, `where`, `alias`, `join` and `render`, so that each of these queries renders to the SQL `SELECT` statement the tests expect.
```hint Record first, render last
The builder methods can be called in different orders, but the clauses of a SQL statement cannot. Think about what each method should store so that `render()` alone decides the final layout.
```
```hint Compare the two condition values
The products example passes `300` for one condition and `"1337"` for the other. Decide how each Python type becomes a SQL literal.
```
### Constraints and Clarifications
- The expected SQL strings live in the existing tests and are not reproduced here. `render()` must match them exactly, so formatting details are confirmed with the interviewer or read from the tests, not guessed.
- Conditions are created with `o.cond(column, operator, value)`. The examples use the operators `>` and `=`, with an integer value (`300`) and a string value (`"1337"`).
- `where` accepts several conditions in one call.
- `join` takes another query and a `using` column that both tables share. In the example, the join column `user_id` is selected on the left side only.
- Every table, column and alias name in the examples is a plain identifier.
### Clarifying Questions
- What exact format do the tests expect: keyword case, spacing and commas, whether `AS` appears before an alias, and whether the statement ends with a semicolon?
- When a query has an alias, are its selected columns qualified with it (`p.price`) even without a join?
- Are several conditions combined with `AND`? Can `where` be called more than once, and do the conditions then accumulate?
- Which operators besides `>` and `=` must `cond` accept, and how should a `None` value render?
- Values are inlined into the returned string. How should a string that contains a single quote be rendered?
- Does each method return a new query, or change the query it is called on? The join example does not assign the result of `users.join(...)`.
- In a joined query, which columns appear in the `SELECT` list, and in what order?
### What a Strong Answer Covers
- An internal representation of the query (table, columns, conditions, alias, joins) that is rendered once, so clause order never depends on call order.
- Literal rendering by type: numbers bare, strings quoted with embedded quotes escaped, and a stated rule for `None` and booleans.
- Alias-qualified names in joins, and a `USING` clause that matches the expected output.
- A deliberate choice between returning new query objects and mutating in place, justified against the join example.
- Validation of operators and identifiers, and a quick check of every example before extending the API.
### Follow-up Questions
- How would you change `render()` to return a parameterized statement plus a list of bound values, and why is that safer than inlining?
- How would you add a join on an arbitrary condition (`on=`) and a left outer join?
- How would you support `OR` and nested groups of conditions while keeping the call sites readable?
- Where would `order_by` and `limit` live in your representation, and how would they render in a joined query?
Overview: Implement a small Python query builder with a fluent API covering table, select, where with condition objects, alias and a join on a shared column, where render() must return the exact SQL string an existing test suite expects. It tests clause ordering, literal quoting, alias qualification and builder design.