spooder 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,9 @@
10
10
 
11
11
  The design goal behind `spooder` is not to provide a full-featured web server, but to expand the Bun runtime with a set of APIs and utilities that make it easy to develop servers with minimal overhead.
12
12
 
13
+ ### spooderverse
14
+ In addition to the core API provided here, you can also find [spooderverse](https://github.com/Kruithne/spooderverse) which is a collection of drop-in modules designed for spooder with minimal overhead and zero dependencies.
15
+
13
16
  > [!NOTE]
14
17
  > If you think a is missing a feature, consider opening an issue with your use-case. The goal behind `spooder` is to provide APIs that are useful for a wide range of use-cases, not to provide bespoke features better suited for userland.
15
18
 
@@ -108,10 +111,8 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
108
111
  - [API > Cache Busting](#api-cache-busting)
109
112
  - [API > Git](#api-git)
110
113
  - [API > Database](#api-database)
114
+ - [API > Database > Utilities](#api-database-utilities)
111
115
  - [API > Database > Schema](#api-database-schema)
112
- - [API > Database > Interface](#api-database-interface)
113
- - [API > Database > Interface > SQLite](#api-database-interface-sqlite)
114
- - [API > Database > Interface > MySQL](#api-database-interface-mysql)
115
116
  - [API > Utilities](#api-utilities)
116
117
 
117
118
  # CLI
@@ -672,46 +673,20 @@ cache_bust_get_hash_table(): Record<string, string>;
672
673
 
673
674
  // git
674
675
  git_get_hashes(length: number): Promise<Record<string, string>>;
675
- git_get_hashes_sync(length: number): Record<string, string>
676
-
677
- // database interface
678
- db_sqlite(filename: string, options: number|object): db_sqlite;
679
- db_mysql(options: ConnectionOptions, pool: boolean): Promise<MySQLDatabaseInterface>;
680
- db_cast_set<T extends string>(set: string | null): Set<T>;
681
- db_serialize_set<T extends string>(set: Set<T> | null): string;
682
-
683
- // db_sqlite
684
- update_schema(db_dir: string, schema_table?: string): Promise<void>
685
- insert(sql: string, ...values: any): number;
686
- insert_object(table: string, obj: Record<string, any>): number;
687
- execute(sql: string, ...values: any): number;
688
- get_all<T>(sql: string, ...values: any): T[];
689
- get_single<T>(sql: string, ...values: any): T | null;
690
- get_column<T>(sql: string, column: string, ...values: any): T[];
691
- get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>;
692
- count(sql: string, ...values: any): number;
693
- count_table(table_name: string): number;
694
- exists(sql: string, ...values: any): boolean;
695
- transaction(scope: (transaction: SQLiteDatabaseInterface) => void | Promise<void>): boolean;
696
-
697
- // db_mysql
698
- update_schema(db_dir: string, schema_table?: string): Promise<void>
699
- insert(sql: string, ...values: any): Promise<number>;
700
- insert_object(table: string, obj: Record<string, any>): Promise<number>;
701
- execute(sql: string, ...values: any): Promise<number>;
702
- get_all<T>(sql: string, ...values: any): Promise<T[]>;
703
- get_single<T>(sql: string, ...values: any): Promise<T | null>;
704
- get_column<T>(sql: string, column: string, ...values: any): Promise<T[]>;
705
- call<T>(func_name: string, ...args: any): Promise<T[]>;
706
- get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>;
707
- count(sql: string, ...values: any): Promise<number>;
708
- count_table(table_name: string): Promise<number>;
709
- exists(sql: string, ...values: any): Promise<boolean>;
710
- transaction(scope: (transaction: MySQLDatabaseInterface) => void | Promise<void>): Promise<boolean>;
676
+ git_get_hashes_sync(length: number): Record<string, string>;
677
+
678
+ // database utilities
679
+ db_set_cast<T extends string>(set: string | null): Set<T>;
680
+ db_set_serialize<T extends string>(set: Iterable<T> | null): string;
711
681
 
712
682
  // database schema
713
- db_update_schema_sqlite(db: Database, schema_dir: string, schema_table?: string): Promise<void>;
714
- db_update_schema_mysql(db: Connection, schema_dir: string, schema_table?: string): Promise<void>;
683
+ type SchemaOptions = {
684
+ schema_table: string;
685
+ recursive: boolean;
686
+ };
687
+
688
+ db_get_schema_revision(db: SQL): Promise<number|null>;
689
+ db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean>;
715
690
 
716
691
  // caching
717
692
  cache_http(options?: CacheOptions);
@@ -746,6 +721,13 @@ log('Hello, {world}!');
746
721
  // > [info] Hello, world!
747
722
  ```
748
723
 
724
+ Tagged template literals are also supported and automatically highlights values without the brace syntax.
725
+
726
+ ```ts
727
+ const user = 'Fred';
728
+ log`Hello ${user}!`;
729
+ ```
730
+
749
731
  Formatting parameters are supported using standard console logging formatters.
750
732
 
751
733
  ```ts
@@ -2528,525 +2510,116 @@ const full_hashes = await git_get_hashes(40);
2528
2510
 
2529
2511
 
2530
2512
  <a id="api-database"></a>
2531
- <a id="api-database-interface"></a>
2532
2513
  ## API > Database
2533
2514
 
2534
- ### 🔧 ``db_cast_set<T extends string>(set: string | null): Set<T>``
2515
+ Before `v6.0.0`, spooder provided a database API for `sqlite` and `mysql` while they were not available natively in `bun`.
2535
2516
 
2536
- Takes a database SET string and returns a `Set<T>` where `T` is a provided enum.
2517
+ Now that `bun` provides a native API for these, we've dropped our API in favor of those as it aligns with the mission of minimalism.
2537
2518
 
2538
- ```ts
2539
- enum ExampleRow {
2540
- OPT_A = 'OPT_A',
2541
- OPT_B = 'OPT_B',
2542
- OPT_C = 'OPT_C'
2543
- };
2519
+ You can see the documentation for the [Bun SQL API here.](https://bun.com/docs/runtime/sql)
2544
2520
 
2545
- const set = db_cast_set<ExampleRow>('OPT_A,OPT_B');
2546
- if (set.has(ExampleRow.OPT_B)) {
2547
- // ...
2548
- }
2549
- ```
2521
+ <a id="api-database-utilities"></a>
2522
+ ## API > Database > Utilities
2550
2523
 
2551
- ### 🔧 ``db_serialize_set<T extends string>(set: Set<T> | null): string``
2524
+ ### 🔧 ``db_set_cast<T extends string>(set: string | null): Set<T>``
2552
2525
 
2553
- Takes a `Set<T>` and returns a database SET string. If the set is empty or `null`, it returns an empty string.
2526
+ Takes a database SET string and returns a `Set<T>` where `T` is a provided enum.
2554
2527
 
2555
2528
  ```ts
2556
- enum ExampleRow {
2557
- OPT_A = 'OPT_A',
2558
- OPT_B = 'OPT_B',
2559
- OPT_C = 'OPT_C'
2529
+ enum Fruits {
2530
+ Apple = 'Apple',
2531
+ Banana = 'Banana',
2532
+ Lemon = 'Lemon'
2560
2533
  };
2561
2534
 
2562
- const set = new Set<ExampleRow>([ExampleRow.OPT_A, ExampleRow.OPT_B]);
2563
-
2564
- const serialized = db_serialize_set(set);
2565
- // > 'OPT_A,OPT_B'
2566
- ```
2567
-
2568
- <a id="api-database-interface-sqlite"></a>
2569
- ## API > Database > Interface > SQLite
2570
-
2571
- `spooder` provides a simple **SQLite** interface that acts as a wrapper around the Bun SQLite API. The construction parameters match the underlying API.
2572
-
2573
- ```ts
2574
- // see: https://bun.sh/docs/api/sqlite
2575
- const db = db_sqlite(':memory:', { create: true });
2576
- db.instance; // raw access to underlying sqlite instance.
2577
- ```
2578
-
2579
- ### Error Reporting
2580
-
2581
- In the event of an error from SQLite, an applicable value will be returned from interface functions, rather than the error being thrown.
2582
-
2583
- ```ts
2584
- const result = await db.get_single('BROKEN QUERY');
2585
- if (result !== null) {
2586
- // do more stuff with result
2587
- }
2588
- ```
2589
-
2590
- If you have configured the canary reporting feature in spooder, you can instruct the database interface to report errors using this feature with the `use_canary_reporting` parameter.
2591
-
2592
- ```ts
2593
- const db = db_sqlite(':memory', { ... }, true);
2594
- ```
2595
-
2596
- ### 🔧 ``db_sqlite.update_schema(schema_dir: string, schema_table: string): Promise<void>``
2597
-
2598
- `spooder` offers a database schema management system. The `update_schema()` function is a shortcut to call this on the underlying database.
2599
-
2600
- See [API > Database > Schema](#api-database-schema) for information on how schema updating works.
2601
-
2602
- ```ts
2603
- // without interface
2604
- import { db_sqlite, db_update_schema_sqlite } from 'spooder';
2605
- const db = db_sqlite('./my_database.sqlite');
2606
- await db_update_schema_sqlite(db.instance, './schema');
2607
-
2608
- // with interface
2609
- import { db_sqlite } from 'spooder';
2610
- const db = db_sqlite('./my_database.sqlite');
2611
- await db.update_schema('./schema');
2612
- ```
2613
-
2614
- ### 🔧 ``db_sqlite.insert(sql: string, ...values: any): number``
2615
-
2616
- Executes a query and returns the `lastInsertRowid`. Returns `-1` in the event of an error or if `lastInsertRowid` is not provided.
2617
-
2618
- ```ts
2619
- const id = db.insert('INSERT INTO users (name) VALUES(?)', 'test');
2620
- ```
2621
-
2622
- ### 🔧 ``db_sqlite.insert_object(table: string, obj: Record<string, any>): number``
2623
-
2624
- Executes an insert query using object key/value mapping and returns the `lastInsertRowid`. Returns `-1` in the event of an error.
2625
-
2626
- ```ts
2627
- const id = db.insert_object('users', { name: 'John', email: 'john@example.com' });
2628
- ```
2629
-
2630
- ### 🔧 ``db_sqlite.execute(sql: string, ...values: any): number``
2631
-
2632
- Executes a query and returns the number of affected rows. Returns `-1` in the event of an error.
2633
-
2634
- ```ts
2635
- const affected = db.execute('UPDATE users SET name = ? WHERE id = ?', 'Jane', 1);
2636
- ```
2637
-
2638
- ### 🔧 ``db_sqlite.get_all<T>(sql: string, ...values: any): T[]``
2639
-
2640
- Returns the complete query result set as an array. Returns empty array if no rows found or if query fails.
2641
-
2642
- ```ts
2643
- const users = db.get_all<User>('SELECT * FROM users WHERE active = ?', true);
2644
- ```
2645
-
2646
- ### 🔧 ``db_sqlite.get_single<T>(sql: string, ...values: any): T | null``
2647
-
2648
- Returns the first row from a query result set. Returns `null` if no rows found or if query fails.
2649
-
2650
- ```ts
2651
- const user = db.get_single<User>('SELECT * FROM users WHERE id = ?', 1);
2652
- ```
2653
-
2654
- ### 🔧 ``db_sqlite.get_column<T>(sql: string, column: string, ...values: any): T[]``
2655
-
2656
- Returns the query result as a single column array. Returns empty array if no rows found or if query fails.
2657
-
2658
- ```ts
2659
- const names = db.get_column<string>('SELECT name FROM users', 'name');
2660
- ```
2661
-
2662
- ### 🔧 ``db_sqlite.get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>``
2663
-
2664
- Returns an async iterator that yields pages of database rows. Each page contains at most `page_size` rows (default 1000).
2665
-
2666
- ```ts
2667
- for await (const page of db.get_paged<User>('SELECT * FROM users', [], 100)) {
2668
- console.log(`Processing ${page.length} users`);
2669
- }
2670
- ```
2671
-
2672
- ### 🔧 ``db_sqlite.count(sql: string, ...values: any): number``
2673
-
2674
- Returns the value of `count` from a query. Returns `0` if query fails.
2675
-
2676
- ```ts
2677
- const user_count = db.count('SELECT COUNT(*) AS count FROM users WHERE active = ?', true);
2678
- ```
2679
-
2680
- ### 🔧 ``db_sqlite.count_table(table_name: string): number``
2681
-
2682
- Returns the total count of rows from a table. Returns `0` if query fails.
2683
-
2684
- ```ts
2685
- const total_users = db.count_table('users');
2686
- ```
2687
-
2688
- ### 🔧 ``db_sqlite.exists(sql: string, ...values: any): boolean``
2689
-
2690
- Returns `true` if the query returns any results. Returns `false` if no results found or if query fails.
2691
-
2692
- ```ts
2693
- const has_active_users = db.exists('SELECT 1 FROM users WHERE active = ? LIMIT 1', true);
2694
- ```
2695
-
2696
- ### 🔧 ``db_sqlite.transaction(scope: (transaction: SQLiteDatabaseInterface) => void | Promise<void>): boolean``
2697
-
2698
- Executes a callback function within a database transaction. The callback receives a transaction object with all the same database methods available. Returns `true` if the transaction was committed successfully, `false` if it was rolled back due to an error.
2699
-
2700
- ```ts
2701
- const success = db.transaction(async (tx) => {
2702
- const user_id = tx.insert('INSERT INTO users (name) VALUES (?)', 'John');
2703
- tx.insert('INSERT INTO user_profiles (user_id, bio) VALUES (?, ?)', user_id, 'Hello world');
2704
- });
2705
-
2706
- if (success) {
2707
- console.log('Transaction completed successfully');
2708
- } else {
2709
- console.log('Transaction was rolled back');
2710
- }
2711
- ```
2712
-
2713
- <a id="api-database-interface-mysql"></a>
2714
- ## API > Database > Interface > MySQL
2715
-
2716
- `spooder` provides a simple **MySQL** interface that acts as a wrapper around the `mysql2` API. The connection options match the underlying API.
2717
-
2718
- > [!IMPORTANT]
2719
- > MySQL requires the optional dependency `mysql2` to be installed - this is not automatically installed with spooder. This will be replaced when bun:sql supports MySQL natively.
2720
-
2721
- ```ts
2722
- // see: https://github.com/mysqljs/mysql#connection-options
2723
- const db = await db_mysql({
2724
- // ...
2725
- });
2726
- db.instance; // raw access to underlying mysql2 instance.
2727
- ```
2728
-
2729
- ### Error Reporting
2730
-
2731
- In the event of an error from MySQL, an applicable value will be returned from interface functions, rather than the error being thrown.
2732
-
2733
- ```ts
2734
- const result = await db.get_single('BROKEN QUERY');
2735
- if (result !== null) {
2736
- // do more stuff with result
2737
- }
2738
- ```
2739
-
2740
- If you have configured the canary reporting feature in spooder, you can instruct the database interface to report errors using this feature with the `use_canary_reporting` parameter.
2741
-
2742
- ```ts
2743
- const db = await db_mysql({ ... }, false, true);
2744
- ```
2745
-
2746
- ### Pooling
2747
-
2748
- MySQL supports connection pooling. This can be configured by providing `true` to the `pool` parameter.
2749
-
2750
- ```ts
2751
- const pool = await db_mysql({ ... }, true);
2752
- ```
2753
-
2754
- ### 🔧 ``db_mysql.update_schema(schema_dir: string, schema_table: string): Promise<void>``
2755
-
2756
- `spooder` offers a database schema management system. The `update_schema()` function is a shortcut to call this on the underlying database.
2757
-
2758
- See [API > Database > Schema](#api-database-schema) for information on how schema updating works.
2759
-
2760
- ```ts
2761
- // without interface
2762
- import { db_mysql, db_update_schema_mysql } from 'spooder';
2763
- const db = await db_mysql({ ... });
2764
- await db_update_schema_mysql(db.instance, './schema');
2765
-
2766
- // with interface
2767
- import { db_mysql } from 'spooder';
2768
- const db = await db_mysql({ ... });
2769
- await db.update_schema('./schema');
2770
- ```
2771
-
2772
- ### 🔧 ``db_mysql.insert(sql: string, ...values: any): Promise<number>``
2773
-
2774
- Executes a query and returns the `LAST_INSERT_ID`. Returns `-1` in the event of an error or if `LAST_INSERT_ID` is not provided.
2775
-
2776
- ```ts
2777
- const id = await db.insert('INSERT INTO tbl (name) VALUES(?)', 'test');
2778
- ```
2779
-
2780
- ### 🔧 ``db_mysql.insert_object(table: string, obj: Record<string, any>): Promise<number>``
2781
-
2782
- Executes an insert query using object key/value mapping and returns the `LAST_INSERT_ID`. Returns `-1` in the event of an error.
2783
-
2784
- ```ts
2785
- const id = await db.insert_object('users', { name: 'John', email: 'john@example.com' });
2786
- ```
2787
-
2788
- ### 🔧 ``db_mysql.execute(sql: string, ...values: any): Promise<number>``
2789
-
2790
- Executes a query and returns the number of affected rows. Returns `-1` in the event of an error.
2791
-
2792
- ```ts
2793
- const affected = await db.execute('UPDATE users SET name = ? WHERE id = ?', 'Jane', 1);
2794
- ```
2795
-
2796
- ### 🔧 ``db_mysql.get_all<T>(sql: string, ...values: any): Promise<T[]>``
2535
+ const [row] = await sql`SELECT * FROM some_table`;
2536
+ const set = db_set_cast<Fruits>(row.fruits);
2797
2537
 
2798
- Returns the complete query result set as an array. Returns empty array if no rows found or if query fails.
2799
-
2800
- ```ts
2801
- const users = await db.get_all<User>('SELECT * FROM users WHERE active = ?', true);
2802
- ```
2803
-
2804
- ### 🔧 ``db_mysql.get_single<T>(sql: string, ...values: any): Promise<T | null>``
2805
-
2806
- Returns the first row from a query result set. Returns `null` if no rows found or if query fails.
2807
-
2808
- ```ts
2809
- const user = await db.get_single<User>('SELECT * FROM users WHERE id = ?', 1);
2810
- ```
2811
-
2812
- ### 🔧 ``db_mysql.get_column<T>(sql: string, column: string, ...values: any): Promise<T[]>``
2813
-
2814
- Returns the query result as a single column array. Returns empty array if no rows found or if query fails.
2815
-
2816
- ```ts
2817
- const names = await db.get_column<string>('SELECT name FROM users', 'name');
2818
- ```
2819
-
2820
- ### 🔧 ``db_mysql.call<T>(func_name: string, ...args: any): Promise<T[]>``
2821
-
2822
- Calls a stored procedure and returns the result set as an array. Returns empty array if no rows found or if query fails.
2823
-
2824
- ```ts
2825
- const results = await db.call<User>('get_active_users', true, 10);
2826
- ```
2827
-
2828
- ### 🔧 ``db_mysql.get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>``
2829
-
2830
- Returns an async iterator that yields pages of database rows. Each page contains at most `page_size` rows (default 1000).
2831
-
2832
- ```ts
2833
- for await (const page of db.get_paged<User>('SELECT * FROM users', [], 100)) {
2834
- console.log(`Processing ${page.length} users`);
2538
+ if (set.has(Fruits.Apple)) {
2539
+ // we have an apple in the set
2835
2540
  }
2836
2541
  ```
2837
2542
 
2838
- ### 🔧 ``db_mysql.count(sql: string, ...values: any): Promise<number>``
2839
-
2840
- Returns the value of `count` from a query. Returns `0` if query fails.
2841
-
2842
- ```ts
2843
- const user_count = await db.count('SELECT COUNT(*) AS count FROM users WHERE active = ?', true);
2844
- ```
2845
-
2846
- ### 🔧 ``db_mysql.count_table(table_name: string): Promise<number>``
2847
-
2848
- Returns the total count of rows from a table. Returns `0` if query fails.
2849
-
2850
- ```ts
2851
- const total_users = await db.count_table('users');
2852
- ```
2853
-
2854
- ### 🔧 ``db_mysql.exists(sql: string, ...values: any): Promise<boolean>``
2543
+ ### 🔧 ``db_set_serialize<T extends string>(set: Iterable<T> | null): string``
2855
2544
 
2856
- Returns `true` if the query returns any results. Returns `false` if no results found or if query fails.
2545
+ Takes an `Iterable<T>` and returns a database SET string. If the set is empty or `null`, it returns an empty string.
2857
2546
 
2858
2547
  ```ts
2859
- const has_active_users = await db.exists('SELECT 1 FROM users WHERE active = ? LIMIT 1', true);
2860
- ```
2548
+ enum Fruits {
2549
+ Apple = 'Apple',
2550
+ Banana = 'Banana',
2551
+ Lemon = 'Lemon'
2552
+ };
2861
2553
 
2862
- ### 🔧 ``db_mysql.transaction(scope: (transaction: MySQLDatabaseInterface) => void | Promise<void>): Promise<boolean>``
2554
+ // edit existing set
2555
+ const [row] = await sql`SELECT * FROM some_table`;
2556
+ const fruits = db_set_cast<Fruits>(row.fruits);
2863
2557
 
2864
- Executes a callback function within a database transaction. The callback receives a transaction object with all the same database methods available. Returns `true` if the transaction was committed successfully, `false` if it was rolled back due to an error.
2558
+ if (!fruits.has(Fruits.Lemon))
2559
+ fruits.add(Fruits.Lemon);
2865
2560
 
2866
- ```ts
2867
- const success = await db.transaction(async (tx) => {
2868
- const user_id = await tx.insert('INSERT INTO users (name) VALUES (?)', 'John');
2869
- await tx.insert('INSERT INTO user_profiles (user_id, bio) VALUES (?, ?)', user_id, 'Hello world');
2870
- });
2561
+ await sql`UPDATE some_table SET fruits = ${sql(db_set_serialize(fruits))} WHERE id = ${row.id}`;
2871
2562
 
2872
- if (success) {
2873
- console.log('Transaction completed successfully');
2874
- } else {
2875
- console.log('Transaction was rolled back');
2876
- }
2563
+ // new set from iterable
2564
+ await sql`UPDATE some_table SET fruits = ${sql(db_set_serialize([Fruits.Apple, Fruits.Lemon]))}`;
2877
2565
  ```
2878
2566
 
2879
2567
  <a id="api-database-schema"></a>
2880
2568
  ## API > Database > Schema
2881
2569
 
2882
- `spooder` provides a straightforward API to manage database schema in revisions through source control.
2570
+ ### 🔧 ``db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean>``
2883
2571
 
2884
- ```ts
2885
- // sqlite
2886
- db_update_schema_sqlite(db: Database, schema_dir: string, schema_table?: string): Promise<void>;
2887
-
2888
- // mysql
2889
- db_update_schema_mysql(db: Connection, schema_dir: string, schema_table?: string): Promise<void>;
2890
- ```
2572
+ `db_schema` executes all revisioned `.sql` files in a given directory, applying them to the database incrementally.
2891
2573
 
2892
2574
  ```ts
2893
- // sqlite example
2894
- import { db_update_schema_sqlite } from 'spooder';
2895
- import { Database } from 'bun:sqlite';
2896
-
2897
- const db = new Database('./database.sqlite');
2898
- await db_update_schema_sqlite(db, './schema');
2575
+ const db = new SQL('db:pw@localhost:3306/test');
2576
+ await db_schema(db, './db/revisions');
2899
2577
  ```
2900
2578
 
2901
- ```ts
2902
- // mysql example
2903
- import { db_update_schema_mysql } from 'spooder';
2904
- import mysql from 'mysql2';
2905
-
2906
- const db = await mysql.createConnection({
2907
- // connection options
2908
- // see https://github.com/mysqljs/mysql#connection-options
2909
- });
2910
- await db_update_schema_mysql(db, './schema');
2911
- ```
2912
-
2913
- > [!IMPORTANT]
2914
- > MySQL requires the optional dependency `mysql2` to be installed - this is not automatically installed with spooder. This will be replaced when bun:sql supports MySQL natively.
2915
-
2916
- ### Interface API
2917
-
2918
- If you are already using the [database interface API](#api-database-interface) provided by `spooder`, you can call `update_schema()` directly on the interface.
2579
+ The above example will **recursively** search the `./db/revisions` directory for all `.sql` files that begin with a positive numeric identifier.
2919
2580
 
2920
2581
  ```ts
2921
- const db = await db_mysql({ ... });
2922
- await db.update_schema('./schema');
2582
+ db/revisions/000_invalid.sql // no: 0 is not valid
2583
+ db/revisions/001_valid.sql // yes: revision 1
2584
+ db/revisions/25-valid.sql // yes: revision 25
2585
+ db/revisions/005_not.txt // no: .sql extension missing
2586
+ db/revisions/invalid_500.sql // no: must begin with rev
2923
2587
  ```
2924
2588
 
2925
- ### Schema Files
2926
-
2927
- The schema directory is expected to contain an SQL file for each table in the database with the file name matching the name of the table.
2928
-
2929
- > [!NOTE]
2930
- > The schema directory is searched recursively and files without the `.sql` extension (case-insensitive) will be ignored.
2589
+ Revisions are applied in **numerical order**, rather than the file sorting order from the operating system. Invalid files are **skipped** without throwing an error.
2931
2590
 
2932
- ```
2933
- - database.sqlite
2934
- - schema/
2935
- - users.sql
2936
- - posts.sql
2937
- - comments.sql
2938
- ```
2591
+ By default, schema revision is tracked in a table called `db_schema`. The name of this table can be customized by providing a different `.schema_table` option.
2939
2592
 
2940
2593
  ```ts
2941
- import { db_update_schema_sqlite } from 'spooder';
2942
- import { Database } from 'bun:sqlite';
2943
-
2944
- const db = new Database('./database.sqlite');
2945
- await db_update_schema_sqlite(db, './schema');
2946
- ```
2947
-
2948
- Each of the SQL files should contain all of the revisions for the table, with the first revision being table creation and subsequent revisions being table modifications.
2949
-
2950
- ```sql
2951
- -- [1] Table creation.
2952
- CREATE TABLE users (
2953
- id INTEGER PRIMARY KEY,
2954
- username TEXT NOT NULL,
2955
- password TEXT NOT NULL
2956
- );
2957
-
2958
- -- [2] Add email column.
2959
- ALTER TABLE users ADD COLUMN email TEXT;
2960
-
2961
- -- [3] Cleanup invalid usernames.
2962
- DELETE FROM users WHERE username = 'admin';
2963
- DELETE FROM users WHERE username = 'root';
2594
+ await db_schema(db, './db/revisions', { schema_table: 'alt_table_name' });
2964
2595
  ```
2965
2596
 
2966
- Each revision should be clearly marked with a comment containing the revision number in square brackets. Anything proceeding the revision number is treated as a comment and ignored.
2967
-
2968
- >[!NOTE]
2969
- > The exact revision header syntax is `^--\s*\[(\d+)\]`.
2970
-
2971
- Everything following a revision header is considered part of that revision until the next revision header or the end of the file, allowing for multiple SQL statements to be included in a single revision.
2972
-
2973
- When calling `db_update_schema_*`, unapplied revisions will be applied in ascending order (regardless of order within the file) until the schema is up-to-date.
2974
-
2975
- It is acceptable to omit keys. This can be useful to prevent repitition when managing stored procedures, views or functions.
2976
-
2977
- ```sql
2978
- -- example of repetitive declaration
2979
-
2980
- -- [1] create view
2981
- CREATE VIEW `view_test` AS SELECT * FROM `table_a` WHERE col = 'foo';
2982
-
2983
- -- [2] change view
2984
- DROP VIEW IF EXISTS `view_test`;
2985
- CREATE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
2986
- ```
2987
- Instead of unnecessarily including each full revision of a procedure, view or function in the schema file, simply store the most up-to-date one and increment the version.
2988
- ```sql
2989
- -- [2] create view
2990
- CREATE OR REPLACE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
2991
- ```
2992
-
2993
-
2994
- Schema revisions are tracked in a table called `db_schema` which is created automatically if it does not exist with the following schema.
2995
-
2996
- ```sql
2997
- CREATE TABLE db_schema (
2998
- db_schema_table_name TEXT PRIMARY KEY,
2999
- db_schema_version INTEGER
3000
- );
3001
- ```
3002
-
3003
- The table used for schema tracking can be changed if necessary by providing an alternative table name as the third paramater.
2597
+ The revision folder is enumerated recursively by default. This can be disabled by passing `false` to `.recursive`, which will only scan the top level of the specified directory.
3004
2598
 
3005
2599
  ```ts
3006
- await db_update_schema_sqlite(db, './schema', 'my_schema_table');
2600
+ await db_schema(db, './db/revisions', { recursive: false });
3007
2601
  ```
3008
2602
 
3009
- >[!IMPORTANT]
3010
- > The entire process is transactional. If an error occurs during the application of **any** revision for **any** table, the entire process will be rolled back and the database will be left in the state it was before the update was attempted.
3011
-
3012
- >[!IMPORTANT]
3013
- > `db_update_schema_*` will throw an error if the revisions cannot be parsed or applied for any reason. It is important you catch and handle appropriately.
3014
-
3015
- ```ts
3016
- try {
3017
- const db = new Database('./database.sqlite');
3018
- await db_update_schema_sqlite(db, './schema');
3019
- } catch (e) {
3020
- // panic (crash) or gracefully continue, etc.
3021
- await panic(e);
3022
- }
3023
- ```
2603
+ Each revision file is executed within a transaction. In the event of an error, the transaction will be rolled back. Successful revision files executed **before** the error will not be rolled back. Subsequent revision files will **not** be executed after an error.
3024
2604
 
3025
- ### Schema Dependencies
3026
- By default, schema files are executed in the order they are provided by the operating system (generally alphabetically). Individual revisions within files are always executed in ascending order.
2605
+ > [!CAUTION]
2606
+ > Implicit commits, such as those that modify DDL, cannot be rolled back inside a transaction.
2607
+ >
2608
+ > It is recommended to only feature one implicit commit query per revision file. In the event of multiple, an error will not rollback previous implicitly committed queries within the revision, leaving your database in a partial state.
2609
+ >
2610
+ > See [MySQL 8.4 Reference Manual // 15.3.3 Statements That Cause an Implicit Commit](https://dev.mysql.com/doc/refman/8.4/en/implicit-commit.html) for more information.
3027
2611
 
3028
- If a specific revision depends on one or more other schema files to be executed before it (for example, when adding foreign keys), you can specify dependencies at the revision level.
3029
2612
 
3030
- ```sql
3031
- -- [1] create table_a (no dependencies)
3032
- CREATE TABLE table_a (
3033
- id INTEGER PRIMARY KEY,
3034
- name TEXT NOT NULL
3035
- );
2613
+ ```ts
2614
+ type SchemaOptions = {
2615
+ schema_table: string;
2616
+ recursive: boolean;
2617
+ };
3036
2618
 
3037
- -- [2] add foreign key to table_b
3038
- -- [deps] table_b_schema.sql
3039
- ALTER TABLE table_a ADD COLUMN table_b_id INTEGER REFERENCES table_b(id);
2619
+ db_get_schema_revision(db: SQL): Promise<number|null>;
2620
+ db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean>;
3040
2621
  ```
3041
2622
 
3042
- When a revision specifies dependencies, all revisions of the dependent schema files will be executed before that specific revision runs. This allows you to create tables independently and then add dependencies in later revisions.
3043
-
3044
- >[!IMPORTANT]
3045
- > Dependencies are specified per-revision, not per-file. A `-- [deps]` line applies only to the revision it appears in.
3046
-
3047
- >[!IMPORTANT]
3048
- > Cyclic or missing dependencies will throw an error.
3049
-
3050
2623
  <a id="api-utilities"></a>
3051
2624
  ## API > Utilities
3052
2625