Add named parameter support for SQL queries
New Feature: Named SQL Parameters - Supports both positional (?) and named (:name) parameters - Named parameters are order-independent and more readable - Syntax: sqlite_param :param_name $variable Implementation: - Updated sqlite_param directive to accept 1 or 2 arguments - ModuleConfig.query_params now stores (name, variable) pairs - execute_query() detects named vs positional parameters - Extracted row_to_map closure to avoid type conflicts - Named params use rusqlite named parameter binding Examples (Port 8082): - Book detail: WHERE id = :book_id - Genre filter: WHERE genre = :genre_name - Year range: WHERE year >= :min_year AND year <= :max_year - Title search: WHERE title LIKE '%' || :search_term || '%' - Rating filter: WHERE rating >= :min_rating Benefits of Named Parameters: - Order-independent: params can be in any order in config - Self-documenting: :book_id is clearer than first ? - Maintainable: can add/remove params without reordering - Recommended for all but simplest queries Configuration: - conf/book_named_params.conf: Complete named params example - start_named_params.sh: Quick start script for port 8082 Documentation: - Added named vs positional comparison in README_PARAMETERS.md - Updated README.md with named parameter examples - Documented both syntaxes in directive reference All examples tested and working with both parameter styles.
This commit is contained in:
50
README.md
50
README.md
@ -48,17 +48,29 @@ A full-featured catalog with category browsing, global templates, and responsive
|
|||||||
|
|
||||||
**See:** `conf/book_catalog.conf` and `README_BOOK_CATALOG.md`
|
**See:** `conf/book_catalog.conf` and `README_BOOK_CATALOG.md`
|
||||||
|
|
||||||
### Example 2: Parameterized Queries (Port 8081)
|
### Example 2: Positional Parameters (Port 8081)
|
||||||
|
|
||||||
Demonstrates dynamic SQL queries with nginx variables.
|
Demonstrates dynamic SQL queries with positional parameters.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Book detail pages by ID
|
- Query parameters with `?` placeholders
|
||||||
- Genre filtering with query parameters
|
- Multiple positional parameters
|
||||||
- Year range searches with multiple parameters
|
- Safe prepared statement binding
|
||||||
- Safe prepared statement parameter binding
|
|
||||||
|
|
||||||
**See:** `conf/book_detail.conf` and `README_PARAMETERS.md`
|
**See:** `conf/book_detail.conf`
|
||||||
|
|
||||||
|
### Example 3: Named Parameters (Port 8082) - Recommended
|
||||||
|
|
||||||
|
Demonstrates named SQL parameters for better readability.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Named parameters with `:name` syntax
|
||||||
|
- Order-independent parameter binding
|
||||||
|
- Title search with LIKE operator
|
||||||
|
- Rating filtering
|
||||||
|
- More maintainable configuration
|
||||||
|
|
||||||
|
**See:** `conf/book_named_params.conf` and `README_PARAMETERS.md`
|
||||||
|
|
||||||
## Configuration Directives
|
## Configuration Directives
|
||||||
|
|
||||||
@ -85,9 +97,14 @@ Specify the Handlebars template file (relative to location path).
|
|||||||
### `sqlite_param`
|
### `sqlite_param`
|
||||||
Add a parameter to the SQL query (can be used multiple times).
|
Add a parameter to the SQL query (can be used multiple times).
|
||||||
|
|
||||||
**Syntax:** `sqlite_param $variable_or_value;`
|
**Syntax:**
|
||||||
|
- Positional: `sqlite_param $variable_or_value;`
|
||||||
|
- Named: `sqlite_param :param_name $variable_or_value;`
|
||||||
|
|
||||||
**Context:** `location`
|
**Context:** `location`
|
||||||
**Notes:** Order matches `?` placeholders in query
|
**Notes:**
|
||||||
|
- Positional parameters match `?` placeholders in order
|
||||||
|
- Named parameters match `:name` placeholders by name (recommended)
|
||||||
|
|
||||||
### `sqlite_global_templates`
|
### `sqlite_global_templates`
|
||||||
Set a directory for global template files (partials, layouts).
|
Set a directory for global template files (partials, layouts).
|
||||||
@ -112,13 +129,22 @@ http {
|
|||||||
sqlite_template "list.hbs";
|
sqlite_template "list.hbs";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parameterized query
|
# Parameterized query with named parameter (recommended)
|
||||||
location = /book {
|
location = /book {
|
||||||
sqlite_db "catalog.db";
|
sqlite_db "catalog.db";
|
||||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
sqlite_query "SELECT * FROM books WHERE id = :book_id";
|
||||||
sqlite_param $arg_id;
|
sqlite_param :book_id $arg_id;
|
||||||
sqlite_template "detail.hbs";
|
sqlite_template "detail.hbs";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Positional parameters also supported
|
||||||
|
location = /search {
|
||||||
|
sqlite_db "catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE year >= ? AND year <= ?";
|
||||||
|
sqlite_param $arg_min; # First ?
|
||||||
|
sqlite_param $arg_max; # Second ?
|
||||||
|
sqlite_template "list.hbs";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -8,13 +8,49 @@ The sqlite-serve module supports parameterized SQL queries using nginx variables
|
|||||||
|
|
||||||
Add parameters to SQL queries. Can be used multiple times to add multiple parameters.
|
Add parameters to SQL queries. Can be used multiple times to add multiple parameters.
|
||||||
|
|
||||||
**Syntax:** `sqlite_param variable_or_value;`
|
**Syntax:**
|
||||||
|
- Positional: `sqlite_param variable_or_value;`
|
||||||
|
- Named: `sqlite_param :param_name variable_or_value;`
|
||||||
|
|
||||||
**Context:** `location`
|
**Context:** `location`
|
||||||
**Multiple:** Yes (order matches `?` placeholders in query)
|
**Multiple:** Yes
|
||||||
|
|
||||||
|
**Note:** Positional parameters match `?` placeholders in order. Named parameters match `:name` placeholders by name.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Query Parameters (Most Common)
|
### Named Parameters (Recommended)
|
||||||
|
|
||||||
|
Named parameters provide better readability and don't depend on order:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location = /book {
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE id = :book_id";
|
||||||
|
sqlite_param :book_id $arg_id; # Named parameter
|
||||||
|
sqlite_template "detail.hbs";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:** `http://localhost/book?id=5`
|
||||||
|
**SQL Executed:** `SELECT * FROM books WHERE id = '5'`
|
||||||
|
|
||||||
|
### Multiple Named Parameters
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location = /years {
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE year >= :min AND year <= :max";
|
||||||
|
sqlite_param :min $arg_min; # Order doesn't matter
|
||||||
|
sqlite_param :max $arg_max; # with named params
|
||||||
|
sqlite_template "list.hbs";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:** `http://localhost/years?min=2015&max=2024`
|
||||||
|
**SQL Executed:** `SELECT * FROM books WHERE year >= '2015' AND year <= '2024'`
|
||||||
|
|
||||||
|
### Query Parameters (Positional)
|
||||||
|
|
||||||
Use nginx's built-in `$arg_*` variables to access query parameters:
|
Use nginx's built-in `$arg_*` variables to access query parameters:
|
||||||
|
|
||||||
@ -267,10 +303,46 @@ Run it with:
|
|||||||
- All SQL placeholders must be `?` (positional parameters)
|
- All SQL placeholders must be `?` (positional parameters)
|
||||||
- Parameters match placeholders in order of `sqlite_param` directives
|
- Parameters match placeholders in order of `sqlite_param` directives
|
||||||
|
|
||||||
|
## Named vs Positional Parameters
|
||||||
|
|
||||||
|
### Named Parameters (`:name` syntax) - Recommended ✓
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- Order-independent: Can rearrange `sqlite_param` directives without breaking queries
|
||||||
|
- Self-documenting: Parameter names explain their purpose
|
||||||
|
- Safer for maintenance: Adding/removing parameters less error-prone
|
||||||
|
- Better for complex queries with many parameters
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```nginx
|
||||||
|
sqlite_query "SELECT * FROM books WHERE author = :author AND year > :year";
|
||||||
|
sqlite_param :year $arg_year; # Order doesn't matter!
|
||||||
|
sqlite_param :author $arg_author;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Positional Parameters (`?` syntax)
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- Slightly more compact configuration
|
||||||
|
- Works well for simple 1-2 parameter queries
|
||||||
|
|
||||||
|
**Disadvantages:**
|
||||||
|
- Order-dependent: Parameters must match `?` placeholders exactly
|
||||||
|
- Less readable with many parameters
|
||||||
|
- Error-prone when modifying queries
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```nginx
|
||||||
|
sqlite_query "SELECT * FROM books WHERE author = ? AND year > ?";
|
||||||
|
sqlite_param $arg_author; # Must be first!
|
||||||
|
sqlite_param $arg_year; # Must be second!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation:** Use named parameters (`:name`) for all but the simplest queries.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- Only supports `?` positional parameters (not named parameters like `:name`)
|
|
||||||
- Parameters must be provided in the exact order they appear in the query
|
|
||||||
- All parameter values are treated as strings (SQLite performs type coercion)
|
- All parameter values are treated as strings (SQLite performs type coercion)
|
||||||
- Complex SQL values (arrays, JSON) should be constructed in the query itself
|
- Complex SQL values (arrays, JSON) should be constructed in the query itself
|
||||||
|
- Cannot mix positional and named parameters in the same query
|
||||||
|
|
||||||
|
|||||||
74
conf/book_named_params.conf
Normal file
74
conf/book_named_params.conf
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Book Catalog with Named Parameters
|
||||||
|
# Demonstrates using named SQL parameters for better readability
|
||||||
|
|
||||||
|
load_module target/debug/libsqlite_serve.dylib;
|
||||||
|
|
||||||
|
worker_processes 1;
|
||||||
|
events {}
|
||||||
|
|
||||||
|
error_log logs/error.log debug;
|
||||||
|
|
||||||
|
http {
|
||||||
|
sqlite_global_templates "server_root/global_templates";
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8082;
|
||||||
|
|
||||||
|
root "server_root";
|
||||||
|
|
||||||
|
# Book detail with named parameter
|
||||||
|
location = /book {
|
||||||
|
add_header "Content-Type" "text/html; charset=utf-8";
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE id = :book_id";
|
||||||
|
sqlite_param :book_id $arg_id;
|
||||||
|
sqlite_template "detail.hbs";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Genre filter with named parameter
|
||||||
|
location = /genre {
|
||||||
|
add_header "Content-Type" "text/html; charset=utf-8";
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE genre = :genre_name ORDER BY rating DESC";
|
||||||
|
sqlite_param :genre_name $arg_genre;
|
||||||
|
sqlite_template "genre.hbs";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Year range with named parameters
|
||||||
|
location = /years {
|
||||||
|
add_header "Content-Type" "text/html; charset=utf-8";
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE year >= :min_year AND year <= :max_year ORDER BY year DESC, title";
|
||||||
|
sqlite_param :min_year $arg_min;
|
||||||
|
sqlite_param :max_year $arg_max;
|
||||||
|
sqlite_template "list.hbs";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search by title with named parameter
|
||||||
|
location = /search {
|
||||||
|
add_header "Content-Type" "text/html; charset=utf-8";
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE title LIKE '%' || :search_term || '%' ORDER BY rating DESC, title";
|
||||||
|
sqlite_param :search_term $arg_q;
|
||||||
|
sqlite_template "list.hbs";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter by rating with named parameter
|
||||||
|
location = /top-rated {
|
||||||
|
add_header "Content-Type" "text/html; charset=utf-8";
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books WHERE rating >= :min_rating ORDER BY rating DESC, title";
|
||||||
|
sqlite_param :min_rating $arg_rating;
|
||||||
|
sqlite_template "list.hbs";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback to all books
|
||||||
|
location / {
|
||||||
|
add_header "Content-Type" "text/html; charset=utf-8";
|
||||||
|
sqlite_db "book_catalog.db";
|
||||||
|
sqlite_query "SELECT * FROM books ORDER BY rating DESC, title";
|
||||||
|
sqlite_template "list.hbs";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
122
server_root/search/list.hbs
Normal file
122
server_root/search/list.hbs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
{{> header}}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.book-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.book-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.book-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
.book-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.book-title {
|
||||||
|
color: #2d3748;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.book-rating {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.book-author {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.book-description {
|
||||||
|
color: #4a5568;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.book-details {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.book-genre {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.book-year {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.book-isbn {
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-item h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.stat-item p {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<h2>📚</h2>
|
||||||
|
<p>Book Collection</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<h2>⭐</h2>
|
||||||
|
<p>Top Rated</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<h2>🔍</h2>
|
||||||
|
<p>Browse All</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="color: #2d3748; margin-bottom: 1rem;">All Books in Collection</h2>
|
||||||
|
|
||||||
|
<div class="book-grid">
|
||||||
|
{{#each results}}
|
||||||
|
{{> book_card}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{> footer}}
|
||||||
|
|
||||||
122
server_root/top-rated/list.hbs
Normal file
122
server_root/top-rated/list.hbs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
{{> header}}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.book-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.book-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.book-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
.book-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.book-title {
|
||||||
|
color: #2d3748;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.book-rating {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.book-author {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.book-description {
|
||||||
|
color: #4a5568;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.book-details {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.book-genre {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.book-year {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.book-isbn {
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-item h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.stat-item p {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<h2>📚</h2>
|
||||||
|
<p>Book Collection</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<h2>⭐</h2>
|
||||||
|
<p>Top Rated</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<h2>🔍</h2>
|
||||||
|
<p>Browse All</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="color: #2d3748; margin-bottom: 1rem;">All Books in Collection</h2>
|
||||||
|
|
||||||
|
<div class="book-grid">
|
||||||
|
{{#each results}}
|
||||||
|
{{> book_card}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{> footer}}
|
||||||
|
|
||||||
71
src/lib.rs
71
src/lib.rs
@ -1,7 +1,7 @@
|
|||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use ngx::core::Buffer;
|
use ngx::core::Buffer;
|
||||||
use ngx::ffi::{
|
use ngx::ffi::{
|
||||||
ngx_hash_key, ngx_http_get_variable, NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF,
|
ngx_hash_key, ngx_http_get_variable, NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_HTTP_LOC_CONF,
|
||||||
NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE, NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE,
|
NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE, NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE,
|
||||||
nginx_version, ngx_chain_t, ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t,
|
nginx_version, ngx_chain_t, ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t,
|
||||||
ngx_module_t, ngx_str_t, ngx_uint_t,
|
ngx_module_t, ngx_str_t, ngx_uint_t,
|
||||||
@ -46,7 +46,7 @@ struct ModuleConfig {
|
|||||||
db_path: String,
|
db_path: String,
|
||||||
query: String,
|
query: String,
|
||||||
template_path: String,
|
template_path: String,
|
||||||
query_params: Vec<String>, // Variable names to use as query parameters
|
query_params: Vec<(String, String)>, // (param_name, variable_name) pairs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global configuration for shared templates
|
// Global configuration for shared templates
|
||||||
@ -179,7 +179,7 @@ static mut ngx_http_howto_commands: [ngx_command_t; 6] = [
|
|||||||
},
|
},
|
||||||
ngx_command_t {
|
ngx_command_t {
|
||||||
name: ngx_string!("sqlite_param"),
|
name: ngx_string!("sqlite_param"),
|
||||||
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
|
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1 | NGX_CONF_TAKE2) as ngx_uint_t,
|
||||||
set: Some(ngx_http_howto_commands_add_param),
|
set: Some(ngx_http_howto_commands_add_param),
|
||||||
conf: NGX_HTTP_LOC_CONF_OFFSET,
|
conf: NGX_HTTP_LOC_CONF_OFFSET,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@ -272,8 +272,20 @@ extern "C" fn ngx_http_howto_commands_add_param(
|
|||||||
unsafe {
|
unsafe {
|
||||||
let conf = &mut *(conf as *mut ModuleConfig);
|
let conf = &mut *(conf as *mut ModuleConfig);
|
||||||
let args = (*(*cf).args).elts as *mut ngx_str_t;
|
let args = (*(*cf).args).elts as *mut ngx_str_t;
|
||||||
let param = (*args.add(1)).to_string();
|
let nelts = (*(*cf).args).nelts;
|
||||||
conf.query_params.push(param);
|
|
||||||
|
if nelts == 2 {
|
||||||
|
// Single argument: positional parameter
|
||||||
|
// sqlite_param $arg_id
|
||||||
|
let variable = (*args.add(1)).to_string();
|
||||||
|
conf.query_params.push((String::new(), variable));
|
||||||
|
} else if nelts == 3 {
|
||||||
|
// Two arguments: named parameter
|
||||||
|
// sqlite_param :book_id $arg_id
|
||||||
|
let param_name = (*args.add(1)).to_string();
|
||||||
|
let variable = (*args.add(2)).to_string();
|
||||||
|
conf.query_params.push((param_name, variable));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::ptr::null_mut()
|
std::ptr::null_mut()
|
||||||
@ -318,7 +330,7 @@ fn load_templates_from_dir(reg: &mut Handlebars, dir_path: &str) -> std::io::Res
|
|||||||
fn execute_query(
|
fn execute_query(
|
||||||
db_path: &str,
|
db_path: &str,
|
||||||
query: &str,
|
query: &str,
|
||||||
params: &[&str],
|
params: &[(String, String)], // (param_name, value) pairs
|
||||||
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
|
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
|
||||||
let conn = Connection::open(db_path)?;
|
let conn = Connection::open(db_path)?;
|
||||||
let mut stmt = conn.prepare(query)?;
|
let mut stmt = conn.prepare(query)?;
|
||||||
@ -328,13 +340,11 @@ fn execute_query(
|
|||||||
.map(|i| stmt.column_name(i).unwrap_or("").to_string())
|
.map(|i| stmt.column_name(i).unwrap_or("").to_string())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Convert params to rusqlite parameters
|
// Bind parameters (either positional or named)
|
||||||
let rusqlite_params: Vec<&dyn rusqlite::ToSql> = params
|
let has_named_params = params.iter().any(|(name, _)| !name.is_empty());
|
||||||
.iter()
|
|
||||||
.map(|p| p as &dyn rusqlite::ToSql)
|
// Convert row to JSON map
|
||||||
.collect();
|
let row_to_map = |row: &rusqlite::Row| -> rusqlite::Result<std::collections::HashMap<String, serde_json::Value>> {
|
||||||
|
|
||||||
let rows = stmt.query_map(rusqlite_params.as_slice(), |row| {
|
|
||||||
let mut map = std::collections::HashMap::new();
|
let mut map = std::collections::HashMap::new();
|
||||||
for (i, col_name) in column_names.iter().enumerate() {
|
for (i, col_name) in column_names.iter().enumerate() {
|
||||||
let value: serde_json::Value = match row.get_ref(i)? {
|
let value: serde_json::Value = match row.get_ref(i)? {
|
||||||
@ -350,7 +360,8 @@ fn execute_query(
|
|||||||
}
|
}
|
||||||
rusqlite::types::ValueRef::Blob(v) => {
|
rusqlite::types::ValueRef::Blob(v) => {
|
||||||
// Convert blob to hex string
|
// Convert blob to hex string
|
||||||
let hex_string = v.iter()
|
let hex_string = v
|
||||||
|
.iter()
|
||||||
.map(|b| format!("{:02x}", b))
|
.map(|b| format!("{:02x}", b))
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
serde_json::Value::String(hex_string)
|
serde_json::Value::String(hex_string)
|
||||||
@ -359,7 +370,23 @@ fn execute_query(
|
|||||||
map.insert(col_name.clone(), value);
|
map.insert(col_name.clone(), value);
|
||||||
}
|
}
|
||||||
Ok(map)
|
Ok(map)
|
||||||
})?;
|
};
|
||||||
|
|
||||||
|
let rows = if has_named_params {
|
||||||
|
// Use named parameters
|
||||||
|
let named_params: Vec<(&str, &dyn rusqlite::ToSql)> = params
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| (name.as_str(), value as &dyn rusqlite::ToSql))
|
||||||
|
.collect();
|
||||||
|
stmt.query_map(named_params.as_slice(), row_to_map)?
|
||||||
|
} else {
|
||||||
|
// Use positional parameters
|
||||||
|
let positional_params: Vec<&dyn rusqlite::ToSql> = params
|
||||||
|
.iter()
|
||||||
|
.map(|(_, value)| value as &dyn rusqlite::ToSql)
|
||||||
|
.collect();
|
||||||
|
stmt.query_map(positional_params.as_slice(), row_to_map)?
|
||||||
|
};
|
||||||
|
|
||||||
rows.collect()
|
rows.collect()
|
||||||
}
|
}
|
||||||
@ -407,8 +434,8 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| {
|
|||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
// Resolve query parameters from nginx variables
|
// Resolve query parameters from nginx variables
|
||||||
let mut param_values: Vec<String> = Vec::new();
|
let mut param_values: Vec<(String, String)> = Vec::new();
|
||||||
for var_name in &co.query_params {
|
for (param_name, var_name) in &co.query_params {
|
||||||
let value = if var_name.starts_with('$') {
|
let value = if var_name.starts_with('$') {
|
||||||
// It's a variable reference, resolve it from nginx
|
// It's a variable reference, resolve it from nginx
|
||||||
let var_name_str = &var_name[1..]; // Remove the '$' prefix
|
let var_name_str = &var_name[1..]; // Remove the '$' prefix
|
||||||
@ -445,19 +472,17 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| {
|
|||||||
// It's a literal value
|
// It's a literal value
|
||||||
var_name.clone()
|
var_name.clone()
|
||||||
};
|
};
|
||||||
param_values.push(value);
|
param_values.push((param_name.clone(), value));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngx_log_debug_http!(
|
ngx_log_debug_http!(
|
||||||
request,
|
request,
|
||||||
"executing query with {} parameters: {:?}",
|
"executing query with {} parameters",
|
||||||
param_values.len(),
|
param_values.len()
|
||||||
param_values
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute the configured SQL query with parameters
|
// Execute the configured SQL query with parameters
|
||||||
let param_refs: Vec<&str> = param_values.iter().map(|s| s.as_str()).collect();
|
let results = match execute_query(&co.db_path, &co.query, ¶m_values) {
|
||||||
let results = match execute_query(&co.db_path, &co.query, ¶m_refs) {
|
|
||||||
Ok(results) => results,
|
Ok(results) => results,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
ngx_log_debug_http!(request, "failed to execute query: {}", e);
|
ngx_log_debug_http!(request, "failed to execute query: {}", e);
|
||||||
|
|||||||
50
start_named_params.sh
Executable file
50
start_named_params.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Start script for named parameters example
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "📚 Starting Named Parameters Example..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if database exists
|
||||||
|
if [ ! -f "book_catalog.db" ]; then
|
||||||
|
echo "Database not found. Running setup..."
|
||||||
|
./setup_book_catalog.sh
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if module is built
|
||||||
|
if [ ! -f "target/debug/libsqlite_serve.dylib" ]; then
|
||||||
|
echo "Module not built. Building..."
|
||||||
|
direnv exec "$PWD" cargo build
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
echo "Starting nginx on http://localhost:8082"
|
||||||
|
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_named_params.conf -p .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Named Parameters Example is running!"
|
||||||
|
echo ""
|
||||||
|
echo "Named Parameter Examples:"
|
||||||
|
echo " • http://localhost:8082/book?id=1"
|
||||||
|
echo " Query: SELECT * FROM books WHERE id = :book_id"
|
||||||
|
echo " Param: :book_id = \$arg_id"
|
||||||
|
echo ""
|
||||||
|
echo " • http://localhost:8082/genre?genre=Programming"
|
||||||
|
echo " Query: ... WHERE genre = :genre_name"
|
||||||
|
echo " Param: :genre_name = \$arg_genre"
|
||||||
|
echo ""
|
||||||
|
echo " • http://localhost:8082/years?min=2015&max=2024"
|
||||||
|
echo " Query: ... WHERE year >= :min_year AND year <= :max_year"
|
||||||
|
echo " Params: :min_year = \$arg_min, :max_year = \$arg_max"
|
||||||
|
echo ""
|
||||||
|
echo " • http://localhost:8082/search?q=Rust"
|
||||||
|
echo " Search by title with named parameter"
|
||||||
|
echo ""
|
||||||
|
echo " • http://localhost:8082/top-rated?rating=4.7"
|
||||||
|
echo " Filter by minimum rating"
|
||||||
|
echo ""
|
||||||
|
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_named_params.conf -p ."
|
||||||
|
|
||||||
Reference in New Issue
Block a user