├── .gitignore ├── Makefile ├── README.md ├── bench └── bench_pg.pony ├── example └── main.pony ├── pg ├── codec │ └── registry.pony ├── connection.pony ├── connection │ ├── conversation.pony │ ├── listener.pony │ ├── manager.pony │ └── tcp.pony ├── introspect │ └── introspect.pony ├── password_providers.pony ├── pg.pony ├── protocol │ ├── client.pony │ ├── common.pony │ └── server.pony └── record.pony └── test └── test_pg.pony /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.sw[p|m|n|o] 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG=pg 2 | BUILD_DIR=build 3 | PONYC=/usr/local/bin/ponyc 4 | PONY_SRC=$(shell find . -name "*.pony") 5 | BIN_DIR=$(BUILD_DIR)/release 6 | BIN=$(BIN_DIR)/example 7 | DEBUG_DIR=$(BUILD_DIR)/debug 8 | DEBUG=$(DEBUG_DIR)/example 9 | TEST_SRC=$(PKG)/test 10 | TEST_BIN=$(BUILD_DIR)/test 11 | BENCH_SRC=$(PKG)/bench 12 | BENCH_BIN=$(BUILD_DIR)/bench 13 | prefix=/usr/local 14 | 15 | all: $(BIN_DIR) test $(BIN) ## Run tests and build the package 16 | 17 | run: $(BIN) ## Build and run the package 18 | $(BIN) 19 | 20 | debug: $(DEBUG) ## Build a and run the package with --debug 21 | $(DEBUG) 22 | 23 | test: $(TEST_BIN) runtest ## Build and run tests 24 | 25 | $(TEST_BIN): $(BUILD_DIR) $(PONY_SRC) 26 | $(PONYC) -o $(BUILD_DIR) --path . $(TEST_SRC) 27 | 28 | runtest: ## Run the tests 29 | $(TEST_BIN) 30 | 31 | bench: $(BENCH_BIN) runbench ## Build and run benchmarks 32 | 33 | $(BENCH_BIN): $(BUILD_DIR) $(PONY_SRC) 34 | $(PONYC) -o $(BUILD_DIR) --path . $(BENCH_SRC) 35 | 36 | runbench: ## Run benchmarks 37 | $(BENCH_BIN) 38 | 39 | $(BUILD_DIR): 40 | mkdir -p $(BUILD_DIR) 41 | 42 | $(BIN_DIR): 43 | mkdir -p $(BIN_DIR) 44 | 45 | $(BIN): $(PONY_SRC) 46 | $(PONYC) -o $(BIN_DIR) -p . example 47 | 48 | $(DEBUG_DIR): 49 | mkdir -p $(DEBUG_DIR) 50 | 51 | $(DEBUG): $(PONY_SRC) 52 | $(PONYC) --debug -p . -o $(DEBUG_DIR) example 53 | 54 | doc: $(PONY_SRC) ## Build the documentation 55 | $(PONYC) -o $(BUILD_DIR) --docs --path . --pass=docs $(PKG) 56 | 57 | clean: ## Remove all artifacts 58 | -rm -rf $(BUILD_DIR) 59 | 60 | 61 | .PHONY: help 62 | 63 | help: ## Show help 64 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pony-pg 2 | 3 | Pure pony PostgreSQL client. Implements a connection pool and an actor-friendly 4 | API. 5 | 6 | ## Status 7 | 8 | Large proof of concept. May be subject to heavy changes or a complete rewrite. 9 | 10 | - Connect to the database (only clear text password) 11 | - Connection pool stub 12 | - Send simple raw queries (no query parameters) and fetchs raw results 13 | in text 14 | - Pluggable password fetching 15 | - RawPasswordProvider: to retieve a hard coded password or a raw string 16 | - EnvPasswordProvider: fetch the password from the standard PGPASSWORD 17 | env var 18 | - The PasswordProviders can be chained 19 | 20 | All these featurea are half-baked at the moment. Status is 'sort-of-works' with 21 | TODOs, Debug.out statements, poor logging and poor error handling. The purpose 22 | of this PoC is to experiment user-facing API and internal design alternatives. 23 | 24 | ## Planned feature 25 | 26 | ### Needed in the proof of concept 27 | 28 | - Cursors (prevents from loading the entire dataset of a result) 29 | - Prepared statements 30 | - Codecs for values 31 | - binary values first 32 | - I prefer coding/decoding binary values than parsing text representation 33 | - The simple query protocol (used by raw queries) returns only text. We'll 34 | have to parse at some point. 35 | - Binary result values 36 | - Query parameters 37 | - Implemented using prepared statements, I don't want to implement escaping 38 | myself, too risky. 39 | - thus, raw queries are not likely to accept parameters any soon. 40 | 41 | ### Later... 42 | 43 | - More auth options (at least MD5) 44 | - Get and set connection parameters (these are currently fetched from the 45 | server but not exposed on the API) 46 | - Transaction Management 47 | - Support logical decoding (I've already wrote a C/Python library for 48 | that, it's heavily asynchronous, it'll be a pleasure to do this in pony) 49 | 50 | ## Roadmap 51 | 52 | ### PoC 53 | 54 | I'm currently working on prepared statements and a couple of codecs for a 55 | couple of types. 56 | 57 | ### Design 58 | 59 | Once it's done we'll have enough to start exploring designs and API. I plan to write 60 | small psql-like client and a CRUD JSON-REST web app to chalange API designs. 61 | 62 | ### Release the best Postgres client library in the world. 63 | 64 | TBD. 65 | 66 | ## Usage 67 | 68 | The API is a moving target at the moment. Please read `example/main.pony`. This 69 | program connects to a database (hard coded name) on localhost, using PGPASSWORD env var. 70 | 71 | Make sure that in postgres' `pg_hba.conf` the authentication method is `password`, not `md5` 72 | 73 | It can be compiled and run with: 74 | 75 | ``` 76 | make run 77 | ``` 78 | 79 | ## Questions 80 | 81 | Testing: 82 | No test at the momeent. It's bad, but the code is evolving too fast. A good test 83 | suite is a must-have for the Design phase. 84 | 85 | For any question, ask me, `lisael` on freenode.net. I'm never far from #ponylang 86 | -------------------------------------------------------------------------------- /bench/bench_pg.pony: -------------------------------------------------------------------------------- 1 | """ 2 | bench_pg.pony 3 | 4 | Bench pg stuff. 5 | """ 6 | 7 | use "ponybench" 8 | use "pg" 9 | 10 | actor Main 11 | let bench: PonyBench 12 | new create(env: Env) => 13 | bench = PonyBench(env) 14 | bench[I32]("Add", lambda(): I32 => I32(2) + 2 end, 1000) 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/main.pony: -------------------------------------------------------------------------------- 1 | """ 2 | main.pony 3 | """ 4 | 5 | use "pg" 6 | use "pg/codec" 7 | use "pg/introspect" 8 | use "net" 9 | use "logger" 10 | use "debug" 11 | use "promises" 12 | 13 | 14 | interface ConnectioHandler is Fulfill[Connection, Connection] 15 | 16 | class BlogEntry 17 | let field1: I32 18 | let field2: I32 19 | let field3: I32 20 | 21 | new create(f1: I32, f2: I32, f3: I32) => 22 | field1 = f1 23 | field2 = f2 24 | field3 = f3 25 | 26 | fun string(): String => 27 | "BlogEntry " + field1.string() + " " + field2.string() + " " + field3.string() 28 | 29 | class User 30 | let id: I32 31 | new create(id': I32) => 32 | id = id' 33 | 34 | fun string(): String => 35 | "User #" + id.string() 36 | 37 | 38 | class BlogEntryRecordNotify is FetchNotify 39 | var entries: Array[BlogEntry val] trn = recover trn Array[BlogEntry val] end 40 | let view: BlogEntriesView tag 41 | 42 | new iso create(v: BlogEntriesView tag) => 43 | view = v 44 | 45 | fun ref record(r: Record val) => 46 | try 47 | let e = recover val BlogEntry( 48 | r(0) as I32, 49 | 2, 3 50 | /*r(1) as I32,*/ 51 | /*r(2) as I32*/ 52 | ) end 53 | entries.push(e) 54 | end 55 | 56 | fun ref stop() => 57 | let entries' = entries = recover trn Array[BlogEntry val] end 58 | view.entries(consume val entries') 59 | 60 | 61 | class UserRecordNotify is FetchNotify 62 | let entries: Array[BlogEntry] = Array[BlogEntry] 63 | let view: BlogEntriesView tag 64 | let logger: Logger[String val] val 65 | 66 | new create(v: BlogEntriesView tag, out: OutStream) => 67 | view = v 68 | logger = StringLogger(Fine, out) 69 | 70 | fun ref descirption(desc: RowDescription) => None 71 | 72 | fun ref batch(b: Array[Record val] val, next: FetchNotifyNext val) => 73 | Debug(b.size()) 74 | for r in b.values() do 75 | try 76 | view.user(recover User(r("id") as I32) end) 77 | else 78 | Debug.out("Error") 79 | end 80 | end 81 | 82 | fun ref record(r: Record val) => 83 | try 84 | view.user(recover User(r("id") as I32) end) 85 | end 86 | 87 | fun ref stop() => None 88 | 89 | 90 | actor BlogEntriesView 91 | var _conn: (Connection tag | None) = None 92 | var _user: ( User val | None ) = None 93 | let _entries: Promise[Array[BlogEntry val] val] = Promise[Array[BlogEntry val] val] 94 | let logger: Logger[String val] val 95 | let out: OutStream 96 | 97 | new create(o: OutStream) => 98 | out = o 99 | logger = StringLogger(Fine, out) 100 | 101 | be fetch_entries() => 102 | try 103 | Debug("fetch_entries") 104 | (_conn as Connection).fetch( 105 | /*"SELECT 1 as user_id, 2, 3 UNION ALL SELECT 4 as user_id, 5, 6 UNION ALL SELECT 7 as user_id, 8, 9",*/ 106 | "SELECT generate_series(0,100)", 107 | recover BlogEntryRecordNotify(this) end) 108 | end 109 | 110 | be fetch_user() => 111 | logger.log("fetch_user") 112 | try 113 | (_conn as Connection).fetch( 114 | "SELECT 1 as id", 115 | recover UserRecordNotify(this, out) end) 116 | end 117 | 118 | be user(u: User iso) => 119 | logger.log("got user #" + u.id.string()) 120 | _user = recover val consume u end 121 | Debug.out("###") 122 | fetch_entries() 123 | 124 | be render(entries': Array[BlogEntry val] val) => 125 | Debug.out("render") 126 | logger.log("render") 127 | try logger.log(entries'.size().string() + " " + entries'(0).string()) end 128 | /*logger.log(entries'.size().string())*/ 129 | try (_conn as Connection).release() end 130 | 131 | be entries(e: Array[BlogEntry val] val) => 132 | Debug.out("fetch") 133 | _entries(e) 134 | 135 | be apply(conn: Connection tag) => 136 | _conn = conn 137 | fetch_user() 138 | _entries.next[None](recover this~render() end) 139 | 140 | 141 | interface iso Hydrate[A: Any #share] 142 | fun apply(record: Record val): A 143 | 144 | 145 | class iso HydrateAll[A: Any #share] 146 | let _h: Hydrate[A] val 147 | 148 | new create(h: Hydrate[A] val) => 149 | _h = h 150 | 151 | fun ref apply(records: Array[Record val] val): Array[A] val => 152 | let result = recover trn Array[A] end 153 | for r in records.values() do 154 | result.push(_h(r)) 155 | end 156 | consume val result 157 | 158 | 159 | class iso HydrateOne[A: Any #share] 160 | let _h: Hydrate[A] val 161 | 162 | new create(h: Hydrate[A] val) => 163 | _h = h 164 | 165 | fun ref apply(records: Array[Record val] val): A ? => 166 | if records.size() != 1 then error end 167 | _h(records(0)) 168 | 169 | 170 | class SQLPromise[A: Any #share] 171 | let _query: String val 172 | let _sess: Session tag 173 | let promise: Promise[Array[Record val] val] = Promise[Array[Record val] val] 174 | var _sent: Bool = false 175 | var _h: Hydrate[A] val 176 | 177 | new iso create(query: String val, session: Session tag, 178 | hydrate: Hydrate[A] val) => 179 | _query = query 180 | _sess = session 181 | _h = hydrate 182 | 183 | fun ref _execute() => 184 | if not _sent then 185 | Debug("execute") 186 | _sess.execute(_query, recover val {(records: Array[Record val] val) => 187 | promise(records) 188 | } end) 189 | _sent = true 190 | end 191 | 192 | fun ref all[B: Any #share]( 193 | fulfill: Fulfill[Array[A] val, B], 194 | rejected: Reject[B] = RejectAlways[B]) 195 | : Promise[B] => 196 | _execute() 197 | let result = promise.next[Array[A] val](recover HydrateAll[A](_h) end) 198 | result.next[B](consume fulfill, consume rejected) 199 | 200 | fun ref one[B: Any #share]( 201 | fulfill: Fulfill[A, B], 202 | rejected: Reject[B] = RejectAlways[B]) 203 | : Promise[B] => 204 | Debug("One") 205 | _execute() 206 | let result = promise.next[A](recover HydrateOne[A](_h) end) 207 | result.next[B](consume fulfill, consume rejected) 208 | 209 | 210 | primitive UserManager 211 | fun table(): String => "user" 212 | fun fields(): Array[String] val => recover val [ 213 | "id" 214 | ] end 215 | fun hydrate(r: Record val): User val=> 216 | try 217 | recover val User(r("id") as I32) end 218 | else 219 | recover val User(42) end 220 | end 221 | 222 | fun by_id(id: I32, sess: Session): SQLPromise[User val]=> 223 | SQLPromise[User val]("SELECT 12 as id;", sess, recover UserManager~hydrate() end) 224 | 225 | 226 | actor RequestContext 227 | let _user: SQLPromise[User val] 228 | 229 | new create(user_id: I32, sess: Session) => 230 | _user = UserManager.by_id(user_id, sess) 231 | 232 | be user(callback: {(User val)} iso) => 233 | Debug("call user") 234 | _user.one[None](consume callback) 235 | 236 | 237 | actor BlogView 238 | let _ctx: RequestContext tag 239 | new create(ctx: RequestContext tag) => 240 | _ctx = ctx 241 | 242 | be render(out: OutStream) => 243 | _ctx.user(recover {(u: User val) => out.write("Hello " + u.string())} end) 244 | 245 | 246 | actor Main 247 | let session: Session 248 | let _env: Env 249 | let logger: Logger[String val] val 250 | 251 | new create(env: Env) => 252 | _env = env 253 | logger = StringLogger(Fine, env.out) 254 | session = Session(env where password=EnvPasswordProvider(env)) 255 | let that = recover tag this end 256 | let bv = BlogView(RequestContext(1, session)) 257 | bv.render(env.out) 258 | bv.render(env.out) 259 | 260 | """ 261 | session.execute("SELECT generate_series(0,1)", 262 | recover val 263 | {(r: Rows val)(that) => 264 | that.raw_count(r) 265 | None 266 | } 267 | end) 268 | 269 | session.execute("SELECT 42, 24 as foo;;", 270 | recover val 271 | {(r: Rows val)(that) => 272 | that.raw_handler(r) 273 | } 274 | end) 275 | 276 | 277 | session.execute("SELECT $1, $2 as foo", 278 | recover val 279 | {(r: Rows val)(that) => 280 | that.execute_handler(r) 281 | } 282 | end, 283 | recover val [as PGValue: I32(70000); I32(-100000)] end) 284 | 285 | let p = session.connect(recover val 286 | {(c: Connection tag)(env) => 287 | BlogEntriesView(env.out)(c) 288 | } 289 | end) 290 | """ 291 | 292 | 293 | be raw_count(rows: Rows val) => 294 | logger.log("rows: " + rows.size().string()) 295 | 296 | be raw_handler(rows: Rows val) => 297 | for row in rows.values() do 298 | try logger.log((row(0) as I32).string()) end 299 | try logger.log((row("foo") as I32).string()) end 300 | end 301 | 302 | be execute_handler(rows: Rows val) => 303 | for row in rows.values() do 304 | try logger.log((row(0) as I32).string()) end 305 | try logger.log((row("foo") as I32).string()) end 306 | end 307 | -------------------------------------------------------------------------------- /pg/codec/registry.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "debug" 3 | 4 | use "pg" 5 | 6 | primitive TypeOid 7 | """ The type oids are found with: 8 | 9 | SELECT 10 | oid, 11 | typname 12 | FROM 13 | pg_catalog.pg_type 14 | WHERE 15 | typtype IN ('b', 'p') 16 | AND (typelem = 0 OR typname = '_oid' OR typname='_text' OR typlen > 0) 17 | AND oid <= 9999 18 | ORDER BY 19 | oid; 20 | 21 | """ 22 | // TODO: Find NULL oid, i'm pretty sure it's not 0 23 | fun apply(t: None): I32 => 0 24 | fun apply(t: Bool val): I32 => 16 25 | fun apply(t: U8 val): I32 => 18 26 | fun apply(t: I64 val): I32 => 20 27 | fun apply(t: I16 val): I32 => 21 28 | fun apply(t: I32 val): I32 => 23 29 | fun apply(t: String val): I32 => 25 30 | fun apply(t: F32 val): I32 => 700 31 | fun apply(t: F64 val): I32 => 701 32 | /*fun apply(t: Any val): I32 => 0*/ 33 | 34 | primitive TypeOids 35 | fun apply(t: Array[PGValue] val): Array[I32] val => 36 | recover val 37 | let result = Array[I32](t.size()) 38 | for item in t.values() do 39 | try result.push(TypeOid(item) as I32) end 40 | end 41 | result 42 | end 43 | 44 | primitive Decode 45 | fun apply(type_oid: I32, value: Array[U8] val, format: I16): PGValue ? => 46 | if format == 0 then 47 | DecodeText(type_oid, value) 48 | else if format == 1 then 49 | DecodeBinary(type_oid, value) 50 | else 51 | Debug.out("Unknown fromat" + format.string()) 52 | error 53 | end end 54 | 55 | primitive DecodeText 56 | fun apply(type_oid: I32, value: Array[U8] val): PGValue ? => 57 | match type_oid 58 | | 23 => String.from_array(value).i32() 59 | //| 23 => I32(1) 60 | else 61 | Debug.out("Unknown type OID: " + type_oid.string()); error 62 | end 63 | 64 | primitive DecodeBinary 65 | fun apply(type_oid: I32, value: Array[U8] val): PGValue ? => 66 | match type_oid 67 | | 23 => 68 | var result = I32(0) 69 | for i in value.values() do 70 | result = (result << 8) + i.i32() 71 | end 72 | result 73 | // I32(1) 74 | else 75 | Debug.out("Unknown type OID: " + type_oid.string()); error 76 | end 77 | 78 | 79 | primitive EncodeBinary 80 | fun apply(param: I32, writer: Writer) ? => 81 | writer.i32_be(param) 82 | fun apply(param: PGValue, writer: Writer) ? => error 83 | -------------------------------------------------------------------------------- /pg/connection.pony: -------------------------------------------------------------------------------- 1 | use "debug" 2 | use "pg/connection" 3 | use "pg/introspect" 4 | 5 | 6 | type FetchNotifyNext is {((FetchNotify iso | None))} 7 | 8 | interface FetchNotify 9 | fun ref descirption(desc: RowDescription) => None 10 | fun ref record(r: Record val) => None 11 | fun ref batch(records: Array[Record val] val, next: FetchNotifyNext val) => 12 | next(None) 13 | for r in records.values() do 14 | record(r) 15 | end 16 | fun ref stop() => None 17 | fun ref server_error() => None 18 | fun size(): USize => 0 19 | 20 | primitive _ReleasAfter 21 | fun apply(c: Connection tag, h: RecordCB val, records: Array[Record val] val) => 22 | h(records) 23 | c.release() 24 | 25 | 26 | actor Connection 27 | let _conn: BEConnection tag 28 | 29 | new create(c: BEConnection tag) => 30 | _conn = c 31 | 32 | be execute(query: String, 33 | handler: RecordCB val, 34 | params: (Array[PGValue] val | None) = None) => 35 | _conn.execute(query, recover val _ReleasAfter~apply(this, handler) end, params) 36 | 37 | be release() => 38 | _conn.terminate() 39 | 40 | be do_terminate() => 41 | Debug.out("Bye") 42 | 43 | be fetch(query: String, notify: FetchNotify iso, 44 | params: (Array[PGValue] val| None) = None) => 45 | _conn.fetch(query, consume notify, params) 46 | -------------------------------------------------------------------------------- /pg/connection/conversation.pony: -------------------------------------------------------------------------------- 1 | use "debug" 2 | use "crypto" 3 | use "logger" 4 | 5 | use "pg/protocol" 6 | use "pg/codec" 7 | use "pg/introspect" 8 | use "pg" 9 | 10 | trait Conversation 11 | 12 | be apply(c: BEConnection tag) 13 | be message(m: ServerMessage val) 14 | 15 | 16 | actor NullConversation is Conversation 17 | let _conn: BEConnection tag 18 | 19 | new create(c: BEConnection tag) => _conn = c 20 | be apply(c: BEConnection tag) => None 21 | be message(m: ServerMessage val) => 22 | _conn.handle_message(m) 23 | 24 | actor AuthConversation is Conversation 25 | let _pool: ConnectionManager 26 | let _params: Array[(String, String)] val 27 | let _conn: BEConnection tag 28 | 29 | new create(p: ConnectionManager, c: BEConnection tag, params: Array[(String, String)] val) => 30 | _pool=p 31 | _conn=c 32 | _params=params 33 | 34 | be log(msg: String) => 35 | _pool.log(msg) 36 | 37 | be apply(c: BEConnection tag) => 38 | let data = recover val 39 | let msg = StartupMessage(_params) 40 | msg.done() 41 | end 42 | c.writev(data) 43 | 44 | be send_clear_pass(pass: String) => 45 | _conn.writev(recover val PasswordMessage(pass).done() end) 46 | 47 | be _send_md5_pass(pass: String, username: String, salt: Array[U8] val) => 48 | // TODO: Make it work. doesn't work at the moment 49 | // from PG doc : concat('md5', md5(concat(md5(concat(password, username)), random-salt))) 50 | var result = "md5" + ToHexString( 51 | MD5( 52 | ToHexString(MD5(pass+username)) + String.from_array(salt) 53 | ) 54 | ) 55 | // Debug(recover val ToHexString(MD5(pass+username)) + String.from_array(salt') end) 56 | // Debug(result) 57 | _conn.writev(recover val PasswordMessage(result).done() end) 58 | 59 | be send_md5_pass(pass: String, req: MD5PwdRequest val) => 60 | Debug.out(pass) 61 | let that = recover tag this end 62 | _pool.get_user(recover {(u: String)(that, pass, req) => that._send_md5_pass(pass, u, req.salt)} end) 63 | 64 | be message(m: ServerMessage val!) => 65 | let that = recover tag this end 66 | match m 67 | | let r: ClearTextPwdRequest val! => 68 | _pool.get_pass(recover {(s: String)(that) => that.send_clear_pass(s)} end) 69 | | let r: MD5PwdRequest val => 70 | _pool.get_pass(recover {(s: String)(that, r) => that.send_md5_pass(s, r)} end) 71 | | let r: AuthenticationOkMessage val => None 72 | | let r: ReadyForQueryMessage val => _conn.next() 73 | else 74 | _conn.handle_message(m) 75 | end 76 | 77 | 78 | class NullFetchNotify is FetchNotify 79 | fun ref batch(records: Array[Record val] val, next: FetchNotifyNext val) => 80 | next(None) 81 | 82 | actor Fetcher 83 | var _notify: FetchNotify iso = recover iso NullFetchNotify end 84 | let _conv: FetchConversation tag 85 | 86 | new create(conv: FetchConversation tag, n: FetchNotify iso) => 87 | _notify = consume n 88 | _conv = conv 89 | 90 | be apply(records: Array[Record val] val, next: FetchNotifyNext val) => 91 | _notify.batch(records, next) 92 | 93 | be set_notifier(fn: (FetchNotify iso | None)) => 94 | match consume fn 95 | | let f: FetchNotify iso => _notify = consume f 96 | end 97 | 98 | be stop() => 99 | _notify.stop() 100 | 101 | 102 | 103 | actor FetchConversation is Conversation 104 | let query: String val 105 | let params: Array[PGValue] val 106 | let _conn: BEConnection tag 107 | var _tuple_desc: (TupleDescription val | None) = None 108 | var _buffer: Array[Record val] trn = recover trn Array[Record val] end 109 | let _size: USize 110 | var _complete: Bool = false 111 | let logger: Logger[String val] val 112 | let fetcher: Fetcher tag 113 | 114 | new create(c: BEConnection tag, q: String, 115 | n: FetchNotify iso, p: Array[PGValue] val, out: OutStream) => 116 | query = q 117 | params = p 118 | _conn = c 119 | _size = n.size() 120 | fetcher = Fetcher(this, consume n) 121 | logger = StringLogger(Warn, out) 122 | 123 | be _batch(b: BatchRowMessage val) => 124 | Debug.out(query) 125 | try 126 | for m in b.rows.values() do 127 | let record = recover val Record(_tuple_desc as TupleDescription val, m.fields) end 128 | _buffer.push(record) 129 | if (_buffer.size() == _size) and (_size > 0) then 130 | _do_send() 131 | end 132 | end 133 | else 134 | Debug.out("can't create and push record") 135 | end 136 | 137 | be _next() => 138 | Debug.out("next") 139 | if not _complete then _execute() else Debug.out("Nope") end 140 | 141 | be _send() => 142 | logger(Fine) and logger.log("coucou") 143 | if _buffer.size() > 0 then 144 | _do_send() 145 | end 146 | 147 | be stop() => 148 | fetcher.stop() 149 | 150 | fun ref _do_send() => 151 | Debug.out("send") 152 | let b = _buffer = recover trn Array[Record val] end 153 | let that = recover tag this end 154 | fetcher(consume val b, recover val 155 | {(fn: (FetchNotify iso | None)=None) (that) => 156 | fetcher.set_notifier(consume fn) 157 | that._next()} 158 | end) 159 | 160 | be message(m: ServerMessage val)=> 161 | match m 162 | | let r: ParseCompleteMessage val => None //_bind() 163 | | let r: CloseCompleteMessage val => _sync() 164 | | let r: BindCompleteMessage val => None //_describe() 165 | | let r: ReadyForQueryMessage val => _conn.next() 166 | | let r: BatchRowMessage val => _batch(r) 167 | | let r: RowDescriptionMessage val => 168 | Debug.out("row_desc") 169 | _tuple_desc = r.tuple_desc 170 | _execute() 171 | | let r: EmptyQueryResponse val => Debug.out("Empty Query") 172 | | let r: CommandCompleteMessage val => 173 | Debug.out("Completed") 174 | _complete = true 175 | _close() 176 | _send() 177 | stop() 178 | | let r: PortalSuspendedMessage val => _send() 179 | else 180 | _conn.handle_message(m) 181 | end 182 | 183 | be log(msg: String) => _conn.log(msg) 184 | 185 | be _sync() => 186 | Debug.out("sync") 187 | _conn.writev(recover val SyncMessage.done() end) 188 | 189 | be _flush() => 190 | Debug.out("flush") 191 | _conn.writev(recover val FlushMessage.done() end) 192 | 193 | be apply(c: BEConnection tag) => 194 | Debug.out("apply") 195 | c.writev(recover val ParseMessage(query, "", TypeOids(params)).done() end) 196 | _bind() 197 | _describe() 198 | 199 | be _bind() => 200 | Debug.out("bind") 201 | _conn.writev(recover val BindMessage("", "", params).done() end) 202 | 203 | be _execute() => 204 | Debug.out("execute") 205 | _flush() 206 | _conn.writev(recover val ExecuteMessage("", _size).done() end) 207 | 208 | be _describe() => 209 | Debug.out("describe") 210 | _flush() 211 | _conn.writev(recover val DescribeMessage('P', "").done() end) 212 | 213 | fun _close() => 214 | Debug.out("close") 215 | _flush() 216 | _conn.writev(recover val CloseMessage('P', "").done() end) 217 | 218 | 219 | actor ExecuteConversation is Conversation 220 | let query: String val 221 | let params: Array[PGValue] val 222 | let _conn: BEConnection tag 223 | let _handler: RecordCB val 224 | var _rows: Rows trn = recover trn Rows end 225 | var _tuple_desc: (TupleDescription val | None) = None 226 | 227 | new create(c: BEConnection tag, q: String, h: RecordCB val, p: Array[PGValue] val) => 228 | query = q 229 | params = p 230 | _conn = c 231 | _handler = h 232 | 233 | be log(msg: String) => _conn.log(msg) 234 | 235 | fun _sync() => 236 | _conn.writev(recover val SyncMessage.done() end) 237 | 238 | fun _flush() => 239 | _conn.writev(recover val FlushMessage.done() end) 240 | 241 | be apply(c: BEConnection tag) => 242 | c.writev(recover val ParseMessage(query, "", TypeOids(params)).done() end) 243 | _flush() 244 | 245 | be _bind() => 246 | _conn.writev(recover val BindMessage("", "", params).done() end) 247 | _flush() 248 | 249 | be _execute() => 250 | _conn.writev(recover val ExecuteMessage("", 0).done() end) 251 | _flush() 252 | 253 | be _describe() => 254 | _conn.writev(recover val DescribeMessage('P', "").done() end) 255 | _flush() 256 | 257 | be _close() => 258 | _conn.writev(recover val CloseMessage('P', "").done() end) 259 | _flush() 260 | 261 | be row(m: DataRowMessage val) => 262 | try 263 | let res = recover val Record(_tuple_desc as TupleDescription val, m.fields) end 264 | _rows.push(res) 265 | end 266 | 267 | be call_back() => 268 | let rows = _rows = recover trn Rows end 269 | _handler(consume val rows) 270 | 271 | be message(m: ServerMessage val)=> 272 | match m 273 | | let r: ParseCompleteMessage val => _bind() 274 | | let r: CloseCompleteMessage val => _sync() 275 | | let r: BindCompleteMessage val => _describe() 276 | | let r: ReadyForQueryMessage val => _conn.next() 277 | | let r: RowDescriptionMessage val => 278 | _tuple_desc = r.tuple_desc 279 | _execute() 280 | | let r: BatchRowMessage val => 281 | for row' in r.rows.values() do 282 | row(row') 283 | end 284 | | let r: EmptyQueryResponse val => Debug.out("Empty Query") 285 | | let r: CommandCompleteMessage val => call_back(); _close() 286 | else 287 | _conn.handle_message(m) 288 | end 289 | 290 | actor QueryConversation is Conversation 291 | let query: String val 292 | let _conn: BEConnection tag 293 | let _handler: RecordCB val 294 | var _rows: Rows trn = recover trn Rows end 295 | var _tuple_desc: (TupleDescription val | None) = None 296 | 297 | new create(c: BEConnection tag, q: String, h: RecordCB val) => 298 | query = q 299 | _conn = c 300 | _handler = h 301 | 302 | be log(msg: String) => _conn.log(msg) 303 | 304 | be apply(c: BEConnection tag) => 305 | c.writev(recover val QueryMessage(query).done() end) 306 | 307 | be call_back() => 308 | let rows = _rows = recover trn Rows end 309 | _handler(consume val rows) 310 | 311 | be row(m: DataRowMessage val) => 312 | try 313 | let res = recover val Record(_tuple_desc as TupleDescription val, m.fields) end 314 | _rows.push(res) 315 | end 316 | 317 | be batch(r: BatchRowMessage val) => 318 | for row' in r.rows.values() do 319 | try 320 | let res = recover val Record(_tuple_desc as TupleDescription val, row'.fields) end 321 | _rows.push(res) 322 | end 323 | end 324 | 325 | be message(m: ServerMessage val) => 326 | match m 327 | | let r: EmptyQueryResponse val => Debug.out("Empty Query") 328 | | let r: CommandCompleteMessage val => call_back(); Debug.out(r.command) 329 | | let r: ReadyForQueryMessage val => _conn.next() 330 | | let r: RowDescriptionMessage val => _tuple_desc = r.tuple_desc 331 | | let r: BatchRowMessage val => 332 | Debug.out("Batch: " + r.rows.size().string() + " rows") 333 | batch(r) 334 | else 335 | _conn.handle_message(m) 336 | end 337 | 338 | actor TerminateConversation is Conversation 339 | let _conn: BEConnection tag 340 | 341 | new create(c: BEConnection tag) => 342 | _conn = c 343 | 344 | be log(msg: String) => _conn.log(msg) 345 | 346 | be apply(c: BEConnection tag) => 347 | c.writev(recover val TerminateMessage.done() end) 348 | 349 | be message(m: ServerMessage val)=> 350 | match m 351 | | let r: ConnectionClosedMessage val => _conn.do_terminate() 352 | else 353 | _conn.handle_message(m) 354 | end 355 | -------------------------------------------------------------------------------- /pg/connection/listener.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "collections" 3 | use "debug" 4 | use "net" 5 | 6 | use "pg/protocol" 7 | use "pg/introspect" 8 | 9 | trait ParseEvent 10 | fun string(): String => "Unknown" 11 | primitive ParsePending is ParseEvent 12 | class PGParseError is ParseEvent 13 | let msg: String 14 | 15 | new iso create(msg': String)=> 16 | msg = msg' 17 | 18 | 19 | class PGNotify is TCPConnectionNotify 20 | 21 | let _conn: _Connection tag 22 | var r: Reader iso = Reader // current reader 23 | var _ctype: U8 = 0 // current type (keep it if the data is chuncked) 24 | var _clen: USize = 0 // current message len (as given by server) 25 | var _batch_size: USize = 1000 // group rows in batch of this size. If 0, the 26 | // batch ends only when the DataRowMessages stop. 27 | var _rows: Array[DataRowMessage val] trn = recover trn Array[DataRowMessage val] end 28 | 29 | fun ref connect_failed(conn: TCPConnection ref) => None 30 | 31 | fun ref connected(conn: TCPConnection ref) => 32 | _conn.connected() 33 | 34 | fun ref closed(conn: TCPConnection ref) => 35 | terminate() 36 | _conn.received(ConnectionClosedMessage) 37 | 38 | new iso create(c: _Connection tag) => 39 | _conn = c 40 | 41 | fun ref _batch_send() => 42 | let rows = _rows = recover trn Array[DataRowMessage val] end 43 | Debug.out("Send Batch: " + rows.size().string() + " rows") 44 | _conn.received(BatchRowMessage(consume rows)) 45 | 46 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, times: USize): Bool => 47 | //let data' = recover val (consume data).slice() end 48 | //r.append(data') 49 | Debug.out("received") 50 | r.append(consume data) 51 | 52 | // don't use while r.size() <= _clen do, because the 53 | // continue is unconditionnal. 54 | while true do 55 | //Debug.out("connection buffer size: " + r.size().string()) 56 | match parse_response() 57 | | let result: PGParseError val => Debug.out(result.msg);_conn.log(result.msg) 58 | | let result: ParsePending val => return true 59 | | let result: DataRowMessage val => 60 | //Debug.out("Row") 61 | _rows.push(result) 62 | if _batch_size > 0 then 63 | if 64 | (r.size() < 5) // not enough bytes remain in the buffer, let's 65 | // send the batch while we're waiting for more 66 | or 67 | (_rows.size() >= _batch_size) // max_size reached, send the batch 68 | then 69 | Debug.out(r.size().string()) 70 | _batch_send() 71 | else 72 | continue 73 | end 74 | end 75 | | let result: ServerMessage val => 76 | // if some messages are still in the batch, there's something new, here. 77 | // we first empty the batch 78 | if (_rows.size() > 0) then 79 | Debug.out("servermessage") 80 | _batch_send() 81 | end 82 | _conn.received(result) 83 | end 84 | if r.size() <= _clen then break end 85 | end 86 | true 87 | 88 | fun ref terminate() => 89 | if _ctype == 'E' then 90 | r.append(recover Array[U8].init(0, _clen - r.size()) end) 91 | try _conn.received(recover val parse_response() end as ServerMessage val) end 92 | end 93 | 94 | fun ref parse_type() ? => 95 | if _ctype > 0 then return end 96 | _ctype = r.u8() 97 | 98 | fun ref parse_len() ? => 99 | if _clen > 0 then return end 100 | _clen = r.i32_be().usize() 101 | 102 | fun ref parse_response(): (ServerMessage val|ParseEvent val) => 103 | try 104 | parse_type() 105 | parse_len() 106 | else 107 | /*Debug.out(" Pending")*/ 108 | return ParsePending 109 | end 110 | /*Debug.out(" _ctype: " + _ctype.string())*/ 111 | /*Debug.out(" _clen: " + _clen.string())*/ 112 | if _clen > ( r.size() + 4) then 113 | /*Debug.out(" Pending (_clen: " + _clen.string() + ", r.size: " + r.size().string() + ")" )*/ 114 | return ParsePending 115 | end 116 | Debug(String.from_array(recover val [as U8: _ctype] end)) 117 | let result = match _ctype 118 | | '1' => ParseCompleteMessage 119 | | '2' => BindCompleteMessage 120 | | '3' => CloseCompleteMessage 121 | | 'C' => try 122 | CommandCompleteMessage(parse_single_string()) 123 | else 124 | PGParseError("Couldn't parse cmd complete message") 125 | end 126 | | 'D' => parse_data_row() 127 | | 'E' => parse_err_resp() 128 | | 'I' => EmptyQueryResponse 129 | | 'K' => parse_backend_key_data() 130 | | 'R' => parse_auth_resp() 131 | | 'S' => parse_parameter_status() 132 | | 'T' => parse_row_description() 133 | | 'Z' => parse_ready_for_query() 134 | | 's' => PortalSuspendedMessage 135 | else 136 | try r.block(_clen-4) else return PGParseError("") end 137 | let ret = PGParseError("Unknown message ID " + _ctype.string()) 138 | _ctype = 0 139 | _clen = 0 140 | ret 141 | end 142 | match result 143 | | let res: ServerMessage val => 144 | _ctype = 0 145 | _clen = 0 146 | end 147 | result 148 | 149 | fun ref parse_data_row(): ServerMessage val => 150 | try 151 | let n_fields = r.u16_be() 152 | let f = recover val 153 | let fields: Array[FieldData val]= Array[FieldData val](n_fields.usize()) 154 | for n in Range(0, n_fields.usize()) do 155 | let len = r.i32_be() 156 | let data = recover val r.block(len.usize()) end 157 | // fields.push(recover val FieldData(len, recover val let a = Array[U8]; a.append(consume data); a end) end) 158 | fields.push(recover val FieldData(len.i32(), data) end) 159 | end 160 | fields 161 | end 162 | DataRowMessage(f) 163 | else 164 | PGParseError("Unreachable") 165 | end 166 | 167 | fun ref parse_string(): String val ? => 168 | recover val 169 | let s = String 170 | while true do 171 | let c = r.u8() 172 | if c == 0 then break else s.push(c) end 173 | end 174 | s 175 | end 176 | 177 | fun ref parse_row_description(): ServerMessage val => 178 | let rd = RowDescription 179 | try 180 | let n_fields = r.u16_be().usize() 181 | let field_descs = recover Array[FieldDescription val](n_fields) end 182 | for n in Range(0, n_fields) do 183 | let name = parse_string() 184 | let table_oid = r.i32_be() 185 | let col_number = r.i16_be() 186 | let type_oid = r.i32_be() 187 | let type_size = r.i16_be() 188 | let type_modifier = r.i32_be() 189 | let format = r.i16_be() 190 | let fd = recover val FieldDescription(name, table_oid, col_number, 191 | type_oid, type_size, 192 | type_modifier, format)end 193 | rd.append(fd) 194 | field_descs.push(fd) 195 | end 196 | let td = recover val TupleDescription(recover val consume field_descs end) end 197 | RowDescriptionMessage(recover val consume ref rd end, td) 198 | else 199 | PGParseError("Unreachable") 200 | end 201 | 202 | fun ref parse_single_string(): String ? => 203 | String.from_array(r.block(_clen - 4)) 204 | 205 | fun ref parse_backend_key_data(): ServerMessage val => 206 | try 207 | let pid = r.u32_be() 208 | let key = r.u32_be() 209 | BackendKeyDataMessage(pid, key) 210 | else 211 | PGParseError("Unreachable") 212 | end 213 | 214 | fun ref parse_ready_for_query(): ServerMessage val => 215 | let b = try r.u8() else return PGParseError("Unreachable") end 216 | ReadyForQueryMessage(b) 217 | 218 | fun ref parse_parameter_status(): ServerMessage val => 219 | let item = try 220 | recover val r.block(_clen-4).slice() end 221 | else 222 | return PGParseError("This should never happen") 223 | end 224 | let end_idx = try 225 | item.find(0) 226 | else 227 | return PGParseError("Malformed parameter message") 228 | end 229 | ParameterStatusMessage( 230 | recover val let a = Array[U8]; a.append(item.trim(0, end_idx)); a end, 231 | recover val let a = Array[U8]; a.append(item.trim(end_idx + 1)); a end) 232 | 233 | fun ref parse_auth_resp(): ServerMessage val => 234 | /*Debug.out("parse_auth_resp")*/ 235 | try 236 | let msg_type = r.i32_be() 237 | /*Debug.out(msg_type)*/ 238 | let result: ServerMessage val = match msg_type // auth message type 239 | | 0 => AuthenticationOkMessage 240 | | 3 => ClearTextPwdRequest 241 | | 5 => MD5PwdRequest(recover val [r.u8(); r.u8(); r.u8(); r.u8()] end) 242 | else 243 | PGParseError("Unknown auth message") 244 | end 245 | result 246 | else 247 | PGParseError("Unreachable") 248 | end 249 | 250 | fun ref parse_err_resp(): ServerMessage val => 251 | // TODO: This is ugly. it used to work with other 252 | // capabilities, so I adapted to get a val fields. It copies 253 | // all, it should not. 254 | let it = recover val 255 | let items = Array[(U8, Array[U8] val)] 256 | let fields' = try r.block(_clen - 4) else 257 | return PGParseError("") 258 | end 259 | let fields = recover val (consume fields').slice() end 260 | var pos: USize = 1 261 | var start_pos = pos 262 | let iter = fields.values() 263 | var c = try iter.next() else return PGParseError("Bad error format") end 264 | var typ = c 265 | repeat 266 | //Debug.out(c) 267 | /*Debug.out("#" + pos.string())*/ 268 | if c == 0 then 269 | //Debug.out("*" + typ.string()) 270 | if typ == 0 then break 271 | else 272 | items.push((typ, fields.trim(start_pos, pos))) 273 | start_pos = pos + 1 274 | typ = 0 275 | end 276 | else 277 | if typ == 0 then typ = c end 278 | end 279 | c = try iter.next() else if typ == 0 then break else 0 end end 280 | pos = pos + 1 281 | until false end 282 | items 283 | end 284 | ErrorMessage(it) 285 | 286 | 287 | -------------------------------------------------------------------------------- /pg/connection/manager.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "promises" 3 | use "logger" 4 | 5 | use "pg" 6 | 7 | actor ConnectionManager 8 | let _connections: Array[BEConnection tag] = Array[BEConnection tag] 9 | let _params: Array[(String, String)] val 10 | let _host: String 11 | let _service: String 12 | let _user: String 13 | let _passwd_provider: PasswordProvider tag 14 | var _password: (String | None) = None 15 | let _max_size: USize 16 | let out: OutStream 17 | let logger: Logger[String val] val 18 | 19 | new create(host: String, 20 | service: String, 21 | user: String, 22 | passwd_provider: PasswordProvider tag, 23 | params: Array[Param] val, 24 | out': OutStream, 25 | pool_size: USize = 1 26 | ) => 27 | _params = params 28 | _host = host 29 | _service = service 30 | _passwd_provider = passwd_provider 31 | _user = user 32 | _max_size = pool_size 33 | out = out' 34 | logger = StringLogger(Fine, out) 35 | 36 | be log(msg: String) => 37 | None 38 | 39 | be connect(auth: AmbientAuth, f: ({(Connection tag):(Any)} val | Promise[Connection tag] val)) => 40 | let priv_conn=_Connection(auth, _host, _service, _params, this, out) 41 | _connections.push(priv_conn) 42 | let conn = Connection(priv_conn) 43 | priv_conn.set_frontend(conn) 44 | match recover val f end 45 | | let f': Promise[Connection tag] => f'(conn) 46 | | let f': {(Connection tag):(Any)} val => f'(conn) 47 | end 48 | 49 | be connect_p(auth: AmbientAuth, f: {(Connection tag)} iso) => 50 | let priv_conn=_Connection(auth, _host, _service, _params, this, out) 51 | _connections.push(priv_conn) 52 | let conn = Connection(priv_conn) 53 | priv_conn.set_frontend(conn) 54 | f(conn) 55 | 56 | be get_pass(f: PassCB iso) => 57 | _passwd_provider(consume f) 58 | 59 | be get_user(f: UserCB iso) => 60 | f(_user) 61 | 62 | be terminate() => 63 | for i in Range(0, _connections.size()) do 64 | try _connections.pop().terminate() end 65 | end 66 | 67 | -------------------------------------------------------------------------------- /pg/connection/tcp.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "collections" 3 | use "debug" 4 | use "logger" 5 | 6 | use "pg/introspect" 7 | use "pg/protocol" 8 | use "pg/codec" 9 | use "pg" 10 | 11 | interface BEConnection 12 | be execute(query: String, 13 | handler: RecordCB val, 14 | params: (Array[PGValue] val | None) = None) 15 | be writev(data: ByteSeqIter) 16 | be log(msg: String) 17 | be handle_message(s: ServerMessage val) 18 | be next() 19 | be schedule(conv: Conversation tag) 20 | be terminate() 21 | be received(s: ServerMessage val) 22 | be do_terminate() 23 | be fetch(query: String, notify: FetchNotify iso, 24 | params: (Array[PGValue] val | None) = None) 25 | 26 | 27 | actor _Connection is BEConnection 28 | let _conn: TCPConnection tag 29 | var _fe: ( Connection tag | None) = None // front-end connection 30 | let _pool: ConnectionManager tag 31 | let _params: Array[(String, String)] val 32 | var _convs: List[Conversation tag] = List[Conversation tag] 33 | var _current: Conversation tag 34 | var _backend_key: (U32, U32) = (0, 0) 35 | let out: OutStream 36 | let logger: Logger[String val] val 37 | 38 | new create(auth: AmbientAuth, 39 | host: String, 40 | service: String, 41 | params: Array[(String, String)] val, 42 | pool: ConnectionManager, 43 | out': OutStream 44 | ) => 45 | _conn = TCPConnection(auth, PGNotify(this), host, service) 46 | _pool = pool 47 | _params = params 48 | _current = AuthConversation(_pool, this, _params) 49 | out = out' 50 | logger = StringLogger(Warn, out) 51 | 52 | be writev(data: ByteSeqIter) => 53 | _conn.writev(data) 54 | 55 | fun ref _schedule(conv: Conversation tag) => 56 | match _current 57 | | let n: NullConversation => 58 | _current = conv 59 | _current(this) 60 | else 61 | _convs.push(conv) 62 | end 63 | 64 | be execute(query: String, 65 | handler: RecordCB val, 66 | params: (Array[PGValue] val | None) = None) => 67 | match params 68 | | let p: None => 69 | schedule(QueryConversation(this, query, handler)) 70 | | let p: Array[PGValue] val => 71 | schedule(ExecuteConversation(this, query, handler, p)) 72 | end 73 | 74 | be fetch(query: String, notify: FetchNotify iso, 75 | params: (Array[PGValue] val | None) = None) => 76 | schedule( 77 | FetchConversation(this, query, consume notify, 78 | try 79 | params as Array[PGValue] val 80 | else 81 | recover val Array[PGValue] end 82 | end, out 83 | ) 84 | ) 85 | 86 | be schedule(conv: Conversation tag) => 87 | _schedule(conv) 88 | 89 | be connected() => 90 | _current(this) 91 | 92 | be _set_backend_key(m: BackendKeyDataMessage val) => 93 | _backend_key = m.data 94 | 95 | be log(msg: String) => 96 | Debug.out(msg) 97 | _pool.log(msg) 98 | 99 | be next() => 100 | try 101 | _current = _convs.shift() 102 | _current(this) 103 | else 104 | _current = NullConversation(this) 105 | end 106 | 107 | be update_param(p: ParameterStatusMessage val) => 108 | // TODO: update the parameters and allow the user to query them 109 | None 110 | 111 | be received(s: ServerMessage val) => 112 | logger(Fine) and logger.log("recieved " + s.string()) 113 | _current.message(s) 114 | 115 | be _log_error(m: ErrorMessage val) => 116 | for (tagg, text) in m.items.values() do 117 | let s: String trn = recover trn String(text.size() + 3) end 118 | s.push(tagg) 119 | s.append(": ") 120 | s.append(text) 121 | Debug.out(consume s) 122 | end 123 | 124 | be handle_message(s: ServerMessage val) => 125 | match s 126 | | let m: ParameterStatusMessage val => update_param(m) 127 | | let m: BackendKeyDataMessage val => _set_backend_key(m) 128 | | let m: ErrorMessage val => _log_error(m) 129 | | let m: ConnectionClosedMessage val => log("Disconected") 130 | else 131 | log("Unknown ServerMessage: " + s.string()) 132 | end 133 | 134 | be terminate() => 135 | schedule(TerminateConversation(this)) 136 | 137 | be do_terminate() => 138 | try (_fe as Connection).do_terminate() end 139 | 140 | be set_frontend(c: Connection tag) => 141 | _fe = c 142 | -------------------------------------------------------------------------------- /pg/introspect/introspect.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | 3 | class FieldDescription 4 | let name: String 5 | let table_oid: I32 6 | let col_number: I16 7 | let type_oid: I32 8 | let type_size: I16 9 | let type_modifier: I32 10 | let format: I16 11 | 12 | new val create(name': String val, 13 | table_oid': I32, 14 | col_number': I16, 15 | type_oid': I32, 16 | type_size': I16, 17 | type_modifier': I32, 18 | format': I16) => 19 | name = name' 20 | table_oid = table_oid' 21 | col_number = col_number' 22 | type_oid = type_oid' 23 | type_size = type_size' 24 | type_modifier = type_modifier' 25 | format = format' 26 | 27 | 28 | class RowDescription 29 | let fields: Array[FieldDescription val]= Array[FieldDescription val] 30 | 31 | fun ref append(f: FieldDescription val) => fields.push(f) 32 | 33 | class TupleDescription 34 | let _fields: Array[FieldDescription val] val 35 | let _by_name: Map[String, (USize, FieldDescription val)] = Map[String, (USize, FieldDescription val)] 36 | 37 | new create(fields: Array[FieldDescription val] val) => 38 | _fields = fields 39 | var pos = USize(0) 40 | for d in fields.values() do 41 | _by_name.update(d.name, (pos, d)) 42 | pos = pos + 1 43 | end 44 | 45 | fun apply(idx: (USize| String)): (USize, FieldDescription val) ? => 46 | match idx 47 | | let idx': USize => (idx', _fields(idx')) 48 | | let idx': String => _by_name(idx') 49 | else 50 | error 51 | end 52 | 53 | class FieldData 54 | let len: I32 55 | let data: Array[U8] val 56 | new create(l: I32, d: Array[U8] val) => 57 | len = l 58 | data = d 59 | -------------------------------------------------------------------------------- /pg/password_providers.pony: -------------------------------------------------------------------------------- 1 | use "options" 2 | 3 | interface PasswordProvider 4 | be apply(f: PassCB val) 5 | be chain(p: PasswordProvider tag) 6 | 7 | actor RawPasswordProvider 8 | let _password: String 9 | 10 | new create(p: String) => _password = p 11 | be apply(f: PassCB val) => f(_password) 12 | be chain(p: PasswordProvider tag) => None 13 | 14 | actor EnvPasswordProvider 15 | let _env: Env 16 | var _next: (PasswordProvider tag | None) = None 17 | 18 | new create(e: Env) => _env = e 19 | be chain(p: PasswordProvider tag) => _next = p 20 | be apply(f: PassCB val) => 21 | try 22 | f(EnvVars(_env.vars())("PGPASSWORD")) 23 | else 24 | try (_next as PasswordProvider tag)(f) end 25 | end 26 | -------------------------------------------------------------------------------- /pg/pg.pony: -------------------------------------------------------------------------------- 1 | """ 2 | pg.pony 3 | 4 | Do pg stuff. 5 | """ 6 | use "options" 7 | use "debug" 8 | 9 | use "pg/protocol" 10 | use "pg/connection" 11 | use "logger" 12 | 13 | 14 | interface _StringCB 15 | fun apply(s: String) 16 | interface PassCB is _StringCB 17 | interface UserCB is _StringCB 18 | 19 | type PGValue is (I64 | I32 | None) 20 | 21 | type Param is (String, String) 22 | 23 | actor Session 24 | let _env: Env 25 | let _mgr: ConnectionManager 26 | let logger: Logger[String val] val 27 | 28 | new create(env: Env, 29 | host: (String | None) = None, 30 | service: (String| None) = None, 31 | user: (String | None) = None, 32 | password: (String | PasswordProvider tag| None) = None, 33 | database: (String | None) = None 34 | ) => 35 | _env = env 36 | logger = StringLogger(Fine, env.out) 37 | 38 | // retreive the connection parameters from env if not provided 39 | // TODO: we should implement all options of libpq as well : 40 | // https://www.postgresql.org/docs/current/static/libpq-envars.html 41 | 42 | let user' = try 43 | user as String 44 | else try 45 | EnvVars(env.vars())("PGUSER") 46 | else try 47 | EnvVars(env.vars())("USER") 48 | else 49 | "" 50 | end end end 51 | 52 | let host' = try 53 | host as String 54 | else try 55 | EnvVars(env.vars())("PGHOST") 56 | else 57 | "localhost" 58 | end end 59 | 60 | let service' = try 61 | service as String 62 | else try 63 | EnvVars(env.vars())("PGPORT") 64 | else 65 | "5432" 66 | end end 67 | 68 | let database' = try 69 | database as String 70 | else try 71 | EnvVars(env.vars())("PGDATABASE") 72 | else 73 | user' 74 | end end 75 | 76 | // Define the password strategy 77 | let provider = match password 78 | | None => EnvPasswordProvider(env) 79 | | let p: PasswordProvider tag => p 80 | | let s: String => RawPasswordProvider(s) 81 | else 82 | RawPasswordProvider("") 83 | end 84 | 85 | _mgr = ConnectionManager(host', service', user', provider, 86 | recover val [("user", user'); ("database", database')] end, env.out) 87 | 88 | be log(msg: String) => 89 | _env.out.print(msg) 90 | 91 | be connect(f: {(Connection tag)} val) => 92 | try _mgr.connect(_env.root as AmbientAuth, f) end 93 | 94 | be execute(query: String, 95 | handler: RecordCB val, 96 | params: (Array[PGValue] val | None) = None) => 97 | let f = recover {(c: Connection)(query, params, handler) => 98 | c.execute(query, handler, params) 99 | } end 100 | try _mgr.connect(_env.root as AmbientAuth, consume f) end 101 | 102 | be terminate()=> 103 | _mgr.terminate() 104 | 105 | -------------------------------------------------------------------------------- /pg/protocol/client.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "debug" 3 | 4 | use "pg/codec" 5 | use "pg" 6 | 7 | type _Param is (String, String) 8 | 9 | interface ClientMessage is Message 10 | fun ref _w(): Writer 11 | fun ref _output(): Writer 12 | fun ref done(): Array[ByteSeq] iso^ => _done(0) 13 | 14 | fun ref _zero() => _w().u8(0) 15 | fun ref _u8(u: U8) => _w().u8(u) 16 | fun ref _write(s: String) => _w().write(s) 17 | fun ref _i32(i: I32) => _w().i32_be(i) 18 | fun ref _i16(i: I16) => _w().i16_be(i) 19 | fun ref _parameter(p: PGValue) => 20 | var param = recover ref Writer end 21 | try EncodeBinary(p, param) end 22 | _i32(param.size().i32()) 23 | _w().writev(param.done()) 24 | 25 | fun ref _debug(id: U8): Array[ByteSeq] iso^ => 26 | if id != 0 then _output().u8(id) end 27 | _output().i32_be(_w().size().i32() + 4) 28 | _output().writev(_w().done()) 29 | let out = Reader 30 | for s in _output().done().values() do 31 | try out.append(s as Array[U8] val) end 32 | try out.append((s as String).array()) end 33 | end 34 | let w = Writer 35 | try 36 | for c in out.block(out.size()).values() do 37 | Debug.out(c) 38 | w.u8(c) 39 | end 40 | end 41 | w.done() 42 | 43 | fun ref _done(id: U8): Array[ByteSeq] iso^ => 44 | if id != 0 then _output().u8(id) end 45 | _output().i32_be(_w().size().i32() + 4) 46 | _output().writev(_w().done()) 47 | _output().done() 48 | 49 | class NullClientMessage is ClientMessage 50 | fun ref _output(): Writer => Writer 51 | fun ref _w(): Writer => Writer 52 | fun ref _zero() => None 53 | fun ref _write(s: String) => None 54 | fun ref _u8(u: U8) => None 55 | fun ref _i32(i: I32) => None 56 | fun ref _i16(i: I16) => None 57 | fun ref _done(id: U8): Array[ByteSeq] iso^ => recover Array[ByteSeq] end 58 | fun ref _debug(id: U8): Array[ByteSeq] iso^ => recover Array[ByteSeq] end 59 | fun ref _parameter(p: PGValue) => None 60 | 61 | class StartupMessage is ClientMessage 62 | var _temp: Writer = Writer 63 | var _out: Writer = Writer 64 | fun ref _output(): Writer => _out 65 | fun ref _w(): Writer => _temp 66 | 67 | new create(params: Array[_Param] box) => 68 | _i32(196608) // protocol version 3.0 69 | for (key, value) in params.values() do 70 | add_param(key, value) 71 | end 72 | 73 | fun ref done(): Array[ByteSeq] iso^ => _zero(); _done(0) 74 | 75 | fun ref add_param(key: String, value: String) => 76 | _write(key); _zero() 77 | _write(value); _zero() 78 | 79 | class FlushMessage is ClientMessage 80 | var _temp: Writer = Writer 81 | var _out: Writer = Writer 82 | fun ref _output(): Writer => _out 83 | fun ref _w(): Writer => _temp 84 | 85 | fun ref done(): Array[ByteSeq] iso^ => _done('H') 86 | 87 | class SyncMessage is ClientMessage 88 | var _temp: Writer = Writer 89 | var _out: Writer = Writer 90 | fun ref _output(): Writer => _out 91 | fun ref _w(): Writer => _temp 92 | 93 | fun ref done(): Array[ByteSeq] iso^ => _done('S') 94 | 95 | class TerminateMessage is ClientMessage 96 | var _temp: Writer = Writer 97 | var _out: Writer = Writer 98 | fun ref _output(): Writer => _out 99 | fun ref _w(): Writer => _temp 100 | 101 | fun ref done(): Array[ByteSeq] iso^ => _done('X') 102 | 103 | class PasswordMessage is ClientMessage 104 | var _temp: Writer = Writer 105 | var _out: Writer = Writer 106 | fun ref _output(): Writer => _out 107 | fun ref _w(): Writer => _temp 108 | 109 | new create(pass: String) => _write(pass) 110 | fun ref done(): Array[ByteSeq] iso^ => _done('p') 111 | 112 | class QueryMessage is ClientMessage 113 | var _temp: Writer = Writer 114 | var _out: Writer = Writer 115 | fun ref _output(): Writer => _out 116 | fun ref _w(): Writer => _temp 117 | 118 | new create(q: String) => _write(q) 119 | fun ref done(): Array[ByteSeq] iso^ =>_zero(); _done('Q') 120 | 121 | class DescribeMessage is ClientMessage 122 | var _temp: Writer = Writer 123 | var _out: Writer = Writer 124 | fun ref _output(): Writer => _out 125 | fun ref _w(): Writer => _temp 126 | 127 | new create(typ: U8, name: String) => 128 | _u8(typ) 129 | _write(name) 130 | _zero() 131 | 132 | fun ref done(): Array[ByteSeq] iso^ => _done('D') 133 | 134 | class CloseMessage is ClientMessage 135 | var _temp: Writer = Writer 136 | var _out: Writer = Writer 137 | fun ref _output(): Writer => _out 138 | fun ref _w(): Writer => _temp 139 | 140 | new create(typ: U8, name: String) => 141 | _u8(typ) 142 | _write(name) 143 | _zero() 144 | 145 | fun ref done(): Array[ByteSeq] iso^ => _done('C') 146 | 147 | class ParseMessage is ClientMessage 148 | var _temp: Writer = Writer 149 | var _out: Writer = Writer 150 | fun ref _output(): Writer => _out 151 | fun ref _w(): Writer => _temp 152 | 153 | new create(query: String, name: String, param_types: Array[I32] val) => 154 | _write(name) 155 | _zero() 156 | _write(query) 157 | _zero() 158 | _i16(param_types.size().i16()) 159 | for oid in param_types.values() do 160 | _i32(oid) 161 | end 162 | 163 | fun ref done(): Array[ByteSeq] iso^ => _done('P') 164 | 165 | class BindMessage is ClientMessage 166 | var _temp: Writer = Writer 167 | var _out: Writer = Writer 168 | fun ref _output(): Writer => _out 169 | fun ref _w(): Writer => _temp 170 | 171 | new create(query: String, name: String, params: Array[PGValue] val) => 172 | _write(name) 173 | _zero() 174 | _write(query) 175 | _zero() 176 | _i16(1) 177 | _i16(1) 178 | _i16(params.size().i16()) 179 | for p in params.values() do 180 | _parameter(p) 181 | end 182 | _i16(1) 183 | _i16(1) 184 | 185 | fun ref done(): Array[ByteSeq] iso^ => _done('B') 186 | 187 | class ExecuteMessage is ClientMessage 188 | var _temp: Writer = Writer 189 | var _out: Writer = Writer 190 | fun ref _output(): Writer => _out 191 | fun ref _w(): Writer => _temp 192 | 193 | new create(portal: String, rows: USize) => 194 | _write(portal) 195 | _zero() 196 | _i32(rows.i32()) 197 | 198 | fun ref done(): Array[ByteSeq] iso^ => _done('E') 199 | -------------------------------------------------------------------------------- /pg/protocol/common.pony: -------------------------------------------------------------------------------- 1 | primitive IdleTransction 2 | fun string(): String => "Idle" 3 | primitive ActiveTransaction 4 | fun string(): String => "Active" 5 | primitive ErrorTransaction 6 | fun string(): String => "Error" 7 | primitive UnknownTransactionStatus 8 | fun string(): String => "Unknown" 9 | 10 | type TransactionStatus is (IdleTransction 11 | | ActiveTransaction 12 | | ErrorTransaction 13 | | UnknownTransactionStatus) 14 | 15 | primitive StatusFromByte 16 | fun apply(b: U8): TransactionStatus => 17 | match b 18 | | 'I' => IdleTransction 19 | | 'T' => ActiveTransaction 20 | | 'E' => ErrorTransaction 21 | else 22 | UnknownTransactionStatus 23 | end 24 | 25 | 26 | trait Message 27 | fun string(): String => "Unknown" 28 | 29 | -------------------------------------------------------------------------------- /pg/protocol/server.pony: -------------------------------------------------------------------------------- 1 | use "pg/introspect" 2 | 3 | interface ServerMessage is Message 4 | 5 | // pseudo messages 6 | class ServerMessageBase is ServerMessage 7 | class NullServerMessage is ServerMessage 8 | fun string(): String => "Null" 9 | class ConnectionClosedMessage is ServerMessage 10 | fun string(): String => "Connection closed" 11 | class BatchRowMessage is ServerMessage 12 | let rows: Array[DataRowMessage val] val 13 | new val create(rows': Array[DataRowMessage val] val) => rows = rows' 14 | fun string(): String => "BatchRowMessage" 15 | 16 | // messages descirbed in https://www.postgresql.org/docs/current/static/protocol-message-formats.html 17 | class AuthenticationOkMessage is ServerMessage 18 | fun string(): String => "Authentication OK" 19 | class ClearTextPwdRequest is ServerMessage 20 | fun string(): String => "ClearTextPwdRequest" 21 | class EmptyQueryResponse is ServerMessage 22 | fun string(): String => "Empty query" 23 | class ParseCompleteMessage is ServerMessage 24 | fun string(): String => "Parse complete" 25 | class BindCompleteMessage is ServerMessage 26 | fun string(): String => "Bind complete" 27 | class CloseCompleteMessage is ServerMessage 28 | fun string(): String => "Close complete" 29 | class PortalSuspendedMessage is ServerMessage 30 | fun string(): String => "Portal Suspended" 31 | class MD5PwdRequest is ServerMessage 32 | let salt: Array[U8] val 33 | new val create(salt': Array[U8] val)=> 34 | salt = salt' 35 | fun string(): String => "MD5PwdRequest" 36 | 37 | class ErrorMessage is ServerMessage 38 | let items: Array[(U8, Array[U8] val)] val 39 | new val create(it: Array[(U8, Array[U8] val)] val) => 40 | items = it 41 | fun string(): String => "Error" 42 | 43 | class ParameterStatusMessage is ServerMessage 44 | let key: String val 45 | let value: String val 46 | new val create(k: Array[U8] val, v: Array[U8] val) => 47 | key = String.from_array(k) 48 | value = String.from_array(v) 49 | fun string(): String => "Param: " + key + "=" + value 50 | 51 | class ReadyForQueryMessage is ServerMessage 52 | let status: TransactionStatus 53 | new val create(b: U8) => 54 | status = StatusFromByte(b) 55 | fun string(): String => "Ready for query: " + status.string() 56 | 57 | class BackendKeyDataMessage is ServerMessage 58 | let data: (U32, U32) 59 | new val create(pid: U32, key: U32) => 60 | data = (pid,key) 61 | fun string(): String => "BackendKeyDataMessage" 62 | 63 | class CommandCompleteMessage is ServerMessage 64 | let command: String 65 | new val create(c: String) => command = c 66 | fun string(): String => "CommandCompleteMessage" 67 | 68 | class RowDescriptionMessage is ServerMessage 69 | let row: RowDescription val 70 | let tuple_desc: TupleDescription val 71 | new val create(rd: RowDescription val, td: TupleDescription val) => 72 | row = rd 73 | tuple_desc = td 74 | fun string(): String => "RowDescriptionMessage" 75 | 76 | class DataRowMessage is ServerMessage 77 | let fields: Array[FieldData val] val 78 | new val create(f: Array[FieldData val] val) => fields = f 79 | fun string(): String => "DataRowMessage" 80 | -------------------------------------------------------------------------------- /pg/record.pony: -------------------------------------------------------------------------------- 1 | use "pg/introspect" 2 | use "pg/codec" 3 | 4 | class Record 5 | let _desc: TupleDescription val 6 | let _tuple: Array[FieldData val] val 7 | 8 | new create(d: TupleDescription val, t: Array[FieldData val] val ) => 9 | _desc = d 10 | _tuple = t 11 | 12 | fun apply(idx: ( USize | String )): PGValue ? => 13 | (let pos: USize, let d: FieldDescription val) = _desc(idx) 14 | Decode(d.type_oid, _tuple(pos).data, d.format) 15 | // if false then error else I32(1) end 16 | 17 | interface RecordCB 18 | fun apply(iter: Array[Record val] val) 19 | 20 | type Rows is Array[Record val] 21 | -------------------------------------------------------------------------------- /test/test_pg.pony: -------------------------------------------------------------------------------- 1 | """ 2 | test_pg.pony 3 | 4 | Test pg stuff. 5 | """ 6 | 7 | use "ponytest" 8 | use "pg" 9 | 10 | actor Main is TestList 11 | new create(env: Env) => 12 | PonyTest(env, this) 13 | 14 | new make() => 15 | None 16 | 17 | fun tag tests(test: PonyTest) => 18 | test(_TestAdd) 19 | 20 | class iso _TestAdd is UnitTest 21 | 22 | fun name():String => "Contains" 23 | 24 | fun apply(h: TestHelper) => 25 | h.assert_eq[I32](2+2, 4) 26 | 27 | 28 | --------------------------------------------------------------------------------